0
0
Fork 0

Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix-front into 6971-MoveBtnToSubtoolbar

This commit is contained in:
Jon Elias 2024-03-21 12:25:04 +01:00
commit 61e7a77882
107 changed files with 4623 additions and 728 deletions

View File

@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2414.01] - 2024-04-04
### Added
### Changed
### Fixed
## [2400.01] - 2024-01-04 ## [2400.01] - 2024-01-04
### Added ### Added

View File

@ -5,13 +5,13 @@ Lilium frontend
## Install the dependencies ## Install the dependencies
```bash ```bash
bun install pnpm install
``` ```
### Install quasar cli ### Install quasar cli
```bash ```bash
sudo bun install -g @quasar/cli sudo npm install -g @quasar/cli
``` ```
### Start the app in development mode (hot-code reloading, error reporting, etc.) ### Start the app in development mode (hot-code reloading, error reporting, etc.)
@ -23,13 +23,13 @@ quasar dev
### Run unit tests ### Run unit tests
```bash ```bash
bun run test:unit pnpm run test:unit
``` ```
### Run e2e tests ### Run e2e tests
```bash ```bash
npm run test:e2e pnpm run test:e2e
``` ```
### Build the app for production ### Build the app for production

View File

@ -1,6 +1,6 @@
{ {
"name": "salix-front", "name": "salix-front",
"version": "24.12.0", "version": "24.14.0",
"description": "Salix frontend", "description": "Salix frontend",
"productName": "Salix", "productName": "Salix",
"author": "Verdnatura", "author": "Verdnatura",

View File

@ -11,7 +11,7 @@ axios.defaults.baseURL = '/api/';
const onRequest = (config) => { const onRequest = (config) => {
const token = session.getToken(); const token = session.getToken();
if (token.length && config.headers) { if (token.length && !config.headers.Authorization) {
config.headers.Authorization = token; config.headers.Authorization = token;
} }

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { reactive, ref } from 'vue'; import { reactive, ref, onMounted, nextTick } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
@ -16,9 +16,8 @@ const props = defineProps({
}); });
const emit = defineEmits(['onDataSaved']); const emit = defineEmits(['onDataSaved']);
const { t } = useI18n(); const { t } = useI18n();
const bicInputRef = ref(null);
const bankEntityFormData = reactive({ const bankEntityFormData = reactive({
name: null, name: null,
bic: null, bic: null,
@ -32,9 +31,14 @@ const countriesFilter = {
const countriesOptions = ref([]); const countriesOptions = ref([]);
const onDataSaved = (data) => { const onDataSaved = (formData, requestResponse) => {
emit('onDataSaved', data); emit('onDataSaved', formData, requestResponse);
}; };
onMounted(async () => {
await nextTick();
bicInputRef.value.focus();
});
</script> </script>
<template> <template>
@ -50,7 +54,7 @@ const onDataSaved = (data) => {
:title="t('title')" :title="t('title')"
:subtitle="t('subtitle')" :subtitle="t('subtitle')"
:form-initial-data="bankEntityFormData" :form-initial-data="bankEntityFormData"
@on-data-saved="onDataSaved($event)" @on-data-saved="onDataSaved"
> >
<template #form-inputs="{ data, validate }"> <template #form-inputs="{ data, validate }">
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
@ -64,6 +68,7 @@ const onDataSaved = (data) => {
</div> </div>
<div class="col"> <div class="col">
<VnInput <VnInput
ref="bicInputRef"
:label="t('swift')" :label="t('swift')"
v-model="data.bic" v-model="data.bic"
:required="true" :required="true"

View File

@ -0,0 +1,174 @@
<script setup>
import { reactive, ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import FetchData from 'components/FetchData.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnInput from 'src/components/common/VnInput.vue';
import FormModelPopup from './FormModelPopup.vue';
import VnInputDate from './common/VnInputDate.vue';
const emit = defineEmits(['onDataSaved']);
const { t } = useI18n();
const router = useRouter();
const manualInvoiceFormData = reactive({
maxShipped: Date.vnNew(),
});
const formModelPopupRef = ref();
const invoiceOutSerialsOptions = ref([]);
const taxAreasOptions = ref([]);
const ticketsOptions = ref([]);
const clientsOptions = ref([]);
const isLoading = computed(() => formModelPopupRef.value?.isLoading);
const onDataSaved = async (formData, requestResponse) => {
emit('onDataSaved', formData, requestResponse);
if (requestResponse && requestResponse.id)
router.push({ name: 'InvoiceOutSummary', params: { id: requestResponse.id } });
};
</script>
<template>
<FetchData
url="InvoiceOutSerials"
:filter="{ where: { code: { neq: 'R' } }, order: ['code'] }"
@on-fetch="(data) => (invoiceOutSerialsOptions = data)"
auto-load
/>
<FetchData
url="TaxAreas"
:filter="{ order: ['code'] }"
@on-fetch="(data) => (taxAreasOptions = data)"
auto-load
/>
<FetchData
url="Tickets"
:filter="{ fields: ['id', 'nickname'], order: 'shipped DESC', limit: 30 }"
@on-fetch="(data) => (ticketsOptions = data)"
auto-load
/>
<FetchData
url="Clients"
:filter="{ fields: ['id', 'name'], order: 'name ASC', limit: 30 }"
@on-fetch="(data) => (clientsOptions = data)"
auto-load
/>
<FormModelPopup
ref="formModelPopupRef"
:title="t('Create manual invoice')"
url-create="InvoiceOuts/createManualInvoice"
model="invoiceOut"
:form-initial-data="manualInvoiceFormData"
@on-data-saved="onDataSaved"
>
<template #form-inputs="{ data }">
<span v-if="isLoading" class="text-primary invoicing-text">
<QIcon name="warning" class="fill-icon q-mr-sm" size="md" />
{{ t('Invoicing in progress...') }}
</span>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('Ticket')"
:options="ticketsOptions"
hide-selected
option-label="id"
option-value="id"
v-model="data.ticketFk"
@update:model-value="data.clientFk = null"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel> #{{ scope.opt?.id }} </QItemLabel>
<QItemLabel caption>{{
scope.opt?.nickname
}}</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</div>
<span class="row items-center" style="max-width: max-content">{{
t('Or')
}}</span>
<div class="col">
<VnSelectFilter
:label="t('Client')"
:options="clientsOptions"
hide-selected
option-label="name"
option-value="id"
v-model="data.clientFk"
@update:model-value="data.ticketFk = null"
/>
</div>
<div class="col">
<VnInputDate :label="t('Max date')" v-model="data.maxShipped" />
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('Serial')"
:options="invoiceOutSerialsOptions"
hide-selected
option-label="description"
option-value="code"
v-model="data.serial"
:required="true"
/>
</div>
<div class="col">
<VnSelectFilter
:label="t('Area')"
:options="taxAreasOptions"
hide-selected
option-label="code"
option-value="code"
v-model="data.taxArea"
:required="true"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnInput
:label="t('Reference')"
type="textarea"
v-model="data.reference"
fill-input
autogrow
/>
</VnRow>
</template>
</FormModelPopup>
</template>
<style lang="scss" scoped>
.invoicing-text {
display: flex;
justify-content: center;
align-items: center;
color: $primary;
font-size: 24px;
margin-bottom: 8px;
}
</style>
<i18n>
es:
Create manual invoice: Crear factura manual
Ticket: Ticket
Client: Cliente
Max date: Fecha límite
Serial: Serie
Area: Area
Reference: Referencia
Or: O
Invoicing in progress...: Facturación en progreso...
</i18n>

View File

@ -10,6 +10,7 @@ import { useValidator } from 'src/composables/useValidator';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import SkeletonForm from 'components/ui/SkeletonForm.vue'; import SkeletonForm from 'components/ui/SkeletonForm.vue';
import VnConfirm from './ui/VnConfirm.vue'; import VnConfirm from './ui/VnConfirm.vue';
import { tMobile } from 'src/composables/tMobile';
const quasar = useQuasar(); const quasar = useQuasar();
const state = useState(); const state = useState();
@ -43,6 +44,10 @@ const $props = defineProps({
type: Boolean, type: Boolean,
default: true, default: true,
}, },
defaultButtons: {
type: Object,
default: () => {},
},
autoLoad: { autoLoad: {
type: Boolean, type: Boolean,
default: false, default: false,
@ -61,6 +66,10 @@ const $props = defineProps({
type: Function, type: Function,
default: null, default: null,
}, },
clearStoreOnUnmount: {
type: Boolean,
default: true,
},
saveFn: { saveFn: {
type: Function, type: Function,
default: null, default: null,
@ -69,10 +78,6 @@ const $props = defineProps({
const emit = defineEmits(['onFetch', 'onDataSaved']); const emit = defineEmits(['onFetch', 'onDataSaved']);
defineExpose({
save,
});
const componentIsRendered = ref(false); const componentIsRendered = ref(false);
onMounted(async () => { onMounted(async () => {
@ -109,7 +114,12 @@ onBeforeRouteLeave((to, from, next) => {
}); });
onUnmounted(() => { onUnmounted(() => {
state.unset($props.model); // Restauramos los datos originales en el store si se realizaron cambios en el formulario pero no se guardaron, evitando modificaciones erróneas.
if (hasChanges.value) {
state.set($props.model, originalData.value);
return;
}
if ($props.clearStoreOnUnmount) state.unset($props.model);
}); });
const isLoading = ref(false); const isLoading = ref(false);
@ -119,7 +129,19 @@ const hasChanges = ref(!$props.observeFormChanges);
const originalData = ref({ ...$props.formInitialData }); const originalData = ref({ ...$props.formInitialData });
const formData = computed(() => state.get($props.model)); const formData = computed(() => state.get($props.model));
const formUrl = computed(() => $props.url); const formUrl = computed(() => $props.url);
const defaultButtons = computed(() => ({
save: {
color: 'primary',
icon: 'restart_alt',
label: 'globals.save',
},
reset: {
color: 'primary',
icon: 'save',
label: 'globals.reset',
},
...$props.defaultButtons,
}));
const startFormWatcher = () => { const startFormWatcher = () => {
watch( watch(
() => formData.value, () => formData.value,
@ -131,10 +153,6 @@ const startFormWatcher = () => {
); );
}; };
function tMobile(...args) {
if (!quasar.platform.is.mobile) return t(...args);
}
async function fetch() { async function fetch() {
const { data } = await axios.get($props.url, { const { data } = await axios.get($props.url, {
params: { filter: JSON.stringify($props.filter) }, params: { filter: JSON.stringify($props.filter) },
@ -205,6 +223,11 @@ watch(formUrl, async () => {
reset(); reset();
fetch(); fetch();
}); });
defineExpose({
save,
isLoading,
});
</script> </script>
<template> <template>
<div class="column items-center full-width"> <div class="column items-center full-width">
@ -233,21 +256,21 @@ watch(formUrl, async () => {
<QBtnGroup push class="q-gutter-x-sm"> <QBtnGroup push class="q-gutter-x-sm">
<slot name="moreActions" /> <slot name="moreActions" />
<QBtn <QBtn
:label="tMobile('globals.reset')" :label="tMobile(defaultButtons.reset.label)"
color="primary" :color="defaultButtons.reset.color"
icon="restart_alt" :icon="defaultButtons.reset.icon"
flat flat
@click="reset" @click="reset"
:disable="!hasChanges" :disable="!hasChanges"
:title="t('globals.reset')" :title="t(defaultButtons.reset.label)"
/> />
<QBtn <QBtn
:label="tMobile('globals.save')" :label="tMobile(defaultButtons.save.label)"
color="primary" :color="defaultButtons.save.color"
icon="save" :icon="defaultButtons.save.icon"
@click="save" @click="save"
:disable="!hasChanges" :disable="!hasChanges"
:title="t('globals.save')" :title="t(defaultButtons.save.label)"
/> />
</QBtnGroup> </QBtnGroup>
</div> </div>

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import FormModel from 'components/FormModel.vue'; import FormModel from 'components/FormModel.vue';
@ -39,27 +39,34 @@ const $props = defineProps({
const { t } = useI18n(); const { t } = useI18n();
const formModelRef = ref(null);
const closeButton = ref(null); const closeButton = ref(null);
const isLoading = ref(false);
const onDataSaved = (dataSaved) => { const onDataSaved = (formData, requestResponse) => {
emit('onDataSaved', dataSaved); emit('onDataSaved', formData, requestResponse);
closeForm(); closeForm();
}; };
const closeForm = () => { const isLoading = computed(() => formModelRef.value?.isLoading);
const closeForm = async () => {
if (closeButton.value) closeButton.value.click(); if (closeButton.value) closeButton.value.click();
}; };
defineExpose({
isLoading,
});
</script> </script>
<template> <template>
<FormModel <FormModel
ref="formModelRef"
:form-initial-data="formInitialData" :form-initial-data="formInitialData"
:observe-form-changes="false" :observe-form-changes="false"
:default-actions="false" :default-actions="false"
:url-create="urlCreate" :url-create="urlCreate"
:model="model" :model="model"
@on-data-saved="onDataSaved($event)" @on-data-saved="onDataSaved"
> >
<template #form="{ data, validate }"> <template #form="{ data, validate }">
<span ref="closeButton" class="close-icon" v-close-popup> <span ref="closeButton" class="close-icon" v-close-popup>

View File

@ -0,0 +1,96 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
const emit = defineEmits(['onSubmit']);
const $props = defineProps({
title: {
type: String,
default: '',
},
subtitle: {
type: String,
default: '',
},
defaultSubmitButton: {
type: Boolean,
default: true,
},
defaultCancelButton: {
type: Boolean,
default: true,
},
customSubmitButtonLabel: {
type: String,
default: '',
},
});
const { t } = useI18n();
const closeButton = ref(null);
const isLoading = ref(false);
const onSubmit = () => {
emit('onSubmit');
closeForm();
};
const closeForm = () => {
if (closeButton.value) closeButton.value.click();
};
</script>
<template>
<QForm
@submit="onSubmit($event)"
class="all-pointer-events full-width"
style="max-width: 800px"
>
<QCard class="q-pa-lg">
<span ref="closeButton" class="close-icon" v-close-popup>
<QIcon name="close" size="sm" />
</span>
<h1 class="title">{{ title }}</h1>
<p>{{ subtitle }}</p>
<slot name="form-inputs" />
<div class="q-mt-lg row justify-end">
<QBtn
v-if="defaultSubmitButton"
:label="customSubmitButtonLabel || t('globals.save')"
type="submit"
color="primary"
:disabled="isLoading"
:loading="isLoading"
/>
<QBtn
v-if="defaultCancelButton"
:label="t('globals.cancel')"
color="primary"
flat
class="q-ml-sm"
:disabled="isLoading"
:loading="isLoading"
v-close-popup
/>
<slot name="customButtons" />
</div>
</QCard>
</QForm>
</template>
<style lang="scss" scoped>
.title {
font-size: 17px;
font-weight: bold;
line-height: 20px;
}
.close-icon {
position: absolute;
top: 20px;
right: 20px;
cursor: pointer;
}
</style>

View File

@ -10,12 +10,12 @@ import UserPanel from 'components/UserPanel.vue';
import VnBreadcrumbs from './common/VnBreadcrumbs.vue'; import VnBreadcrumbs from './common/VnBreadcrumbs.vue';
const { t } = useI18n(); const { t } = useI18n();
const session = useSession();
const stateStore = useStateStore(); const stateStore = useStateStore();
const quasar = useQuasar(); const quasar = useQuasar();
const state = useState(); const state = useState();
const user = state.getUser(); const user = state.getUser();
const token = session.getToken(); const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia();
const appName = 'Lilium'; const appName = 'Lilium';
onMounted(() => stateStore.setMounted()); onMounted(() => stateStore.setMounted());

View File

@ -0,0 +1,168 @@
<script setup>
import { ref, reactive } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import VnRow from 'components/ui/VnRow.vue';
import FetchData from 'components/FetchData.vue';
import VnSelectFilter from 'components/common/VnSelectFilter.vue';
import FormPopup from './FormPopup.vue';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
const $props = defineProps({
invoiceOutData: {
type: Object,
default: () => {},
},
});
const { t } = useI18n();
const router = useRouter();
const { notify } = useNotify();
const transferInvoiceParams = reactive({
id: $props.invoiceOutData?.id,
refFk: $props.invoiceOutData?.ref,
});
const closeButton = ref(null);
const clientsOptions = ref([]);
const rectificativeTypeOptions = ref([]);
const siiTypeInvoiceOutsOptions = ref([]);
const invoiceCorrectionTypesOptions = ref([]);
const closeForm = () => {
if (closeButton.value) closeButton.value.click();
};
const transferInvoice = async () => {
try {
const { data } = await axios.post(
'InvoiceOuts/transferInvoice',
transferInvoiceParams
);
notify(t('Transferred invoice'), 'positive');
closeForm();
router.push('InvoiceOutSummary', { id: data.id });
} catch (err) {
console.error('Error transfering invoice', err);
}
};
</script>
<template>
<FetchData
url="Clients"
@on-fetch="(data) => (clientsOptions = data)"
:filter="{ fields: ['id', 'name'], order: 'id', limit: 30 }"
auto-load
/>
<FetchData
url="CplusRectificationTypes"
:filter="{ order: 'description' }"
@on-fetch="(data) => (rectificativeTypeOptions = data)"
auto-load
/>
<FetchData
url="SiiTypeInvoiceOuts"
:filter="{ where: { code: { like: 'R%' } } }"
@on-fetch="(data) => (siiTypeInvoiceOutsOptions = data)"
auto-load
/>
<FetchData
url="InvoiceCorrectionTypes"
@on-fetch="(data) => (invoiceCorrectionTypesOptions = data)"
auto-load
/>
<FormPopup
@on-submit="transferInvoice()"
:title="t('Transfer invoice')"
:custom-submit-button-label="t('Transfer client')"
:default-cancel-button="false"
>
<template #form-inputs>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('Client')"
:options="clientsOptions"
hide-selected
option-label="name"
option-value="id"
v-model="transferInvoiceParams.newClientFk"
:required="true"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
#{{ scope.opt?.id }} -
{{ scope.opt?.name }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</div>
<div class="col">
<VnSelectFilter
:label="t('Rectificative type')"
:options="rectificativeTypeOptions"
hide-selected
option-label="description"
option-value="id"
v-model="transferInvoiceParams.cplusRectificationTypeFk"
:required="true"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('Class')"
:options="siiTypeInvoiceOutsOptions"
hide-selected
option-label="description"
option-value="id"
v-model="transferInvoiceParams.siiTypeInvoiceOutFk"
:required="true"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
{{ scope.opt?.code }} -
{{ scope.opt?.description }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</div>
<div class="col">
<VnSelectFilter
:label="t('Type')"
:options="invoiceCorrectionTypesOptions"
hide-selected
option-label="description"
option-value="id"
v-model="transferInvoiceParams.invoiceCorrectionTypeFk"
:required="true"
/>
</div>
</VnRow>
</template>
</FormPopup>
</template>
<i18n>
es:
Transfer invoice: Transferir factura
Transfer client: Transferir cliente
Client: Cliente
Rectificative type: Tipo rectificativa
Class: Clase
Type: Tipo
Transferred invoice: Factura transferida
</i18n>

View File

@ -44,7 +44,7 @@ const darkMode = computed({
}); });
const user = state.getUser(); const user = state.getUser();
const token = session.getToken(); const token = session.getTokenMultimedia();
onMounted(async () => { onMounted(async () => {
updatePreferences(); updatePreferences();

View File

@ -0,0 +1,156 @@
<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';
import useNotify from "composables/useNotify";
const MESSAGE_MAX_LENGTH = 160;
const {t} = useI18n();
const {notify} = useNotify();
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`);
}
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);
notify('globals.smsSent', 'positive');
}
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

@ -27,6 +27,10 @@ const $props = defineProps({
type: Object, type: Object,
default: null, default: null,
}, },
url: {
type: String,
default: null,
},
}); });
const warehouses = ref(); const warehouses = ref();
@ -65,14 +69,15 @@ function mapperDms(data) {
} }
function getUrl() { function getUrl() {
if ($props.url) return $props.url;
if ($props.formInitialData) return 'dms/' + $props.formInitialData.id + '/updateFile'; if ($props.formInitialData) return 'dms/' + $props.formInitialData.id + '/updateFile';
return `${$props.model}/${route.params.id}/uploadFile`; return `${$props.model}/${route.params.id}/uploadFile`;
} }
async function save() { async function save() {
const body = mapperDms(dms.value); const body = mapperDms(dms.value);
await axios.post(getUrl(), body[0], body[1]); const response = await axios.post(getUrl(), body[0], body[1]);
emit('onDataSaved', body[1].params); emit('onDataSaved', body[1].params, response);
} }
function defaultData() { function defaultData() {

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { computed } from 'vue'; import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
const emit = defineEmits(['update:modelValue', 'update:options', 'keyup.enter']); const emit = defineEmits(['update:modelValue', 'update:options', 'keyup.enter']);
@ -17,7 +17,7 @@ const $props = defineProps({
const { t } = useI18n(); const { t } = useI18n();
const requiredFieldRule = (val) => !!val || t('globals.fieldRequired'); const requiredFieldRule = (val) => !!val || t('globals.fieldRequired');
const vnInputRef = ref(null);
const value = computed({ const value = computed({
get() { get() {
return $props.modelValue; return $props.modelValue;
@ -40,6 +40,14 @@ const styleAttrs = computed(() => {
const onEnterPress = () => { const onEnterPress = () => {
emit('keyup.enter'); emit('keyup.enter');
}; };
const focus = () => {
vnInputRef.value.focus();
};
defineExpose({
focus,
});
</script> </script>
<template> <template>

View File

@ -59,6 +59,9 @@ const toggleForm = () => {
:name="actionIcon" :name="actionIcon"
:size="actionIcon === 'add' ? 'xs' : 'sm'" :size="actionIcon === 'add' ? 'xs' : 'sm'"
:class="['default-icon', { '--add-icon': actionIcon === 'add' }]" :class="['default-icon', { '--add-icon': actionIcon === 'add' }]"
:style="{
'font-variation-settings': `'FILL' ${1}`,
}"
> >
<QTooltip v-if="tooltip">{{ tooltip }}</QTooltip> <QTooltip v-if="tooltip">{{ tooltip }}</QTooltip>
</QIcon> </QIcon>

View File

@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n';
import SkeletonDescriptor from 'components/ui/SkeletonDescriptor.vue'; import SkeletonDescriptor from 'components/ui/SkeletonDescriptor.vue';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { useState } from 'src/composables/useState';
const $props = defineProps({ const $props = defineProps({
url: { url: {
@ -35,6 +36,8 @@ const $props = defineProps({
default: null, default: null,
}, },
}); });
const state = useState();
const slots = useSlots(); const slots = useSlots();
const { t } = useI18n(); const { t } = useI18n();
const { viewSummary } = useSummaryDialog(); const { viewSummary } = useSummaryDialog();
@ -64,6 +67,7 @@ async function getData() {
isLoading.value = true; isLoading.value = true;
try { try {
const { data } = await arrayData.fetch({ append: false, updateRouter: false }); const { data } = await arrayData.fetch({ append: false, updateRouter: false });
state.set($props.dataKey, data);
emit('onFetch', data); emit('onFetch', data);
} finally { } finally {
isLoading.value = false; isLoading.value = false;

View File

@ -1,11 +1,10 @@
<script setup> <script setup>
import { onMounted, ref, watch } from 'vue'; import { onMounted, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import axios from 'axios'; import axios from 'axios';
import SkeletonSummary from 'components/ui/SkeletonSummary.vue'; import SkeletonSummary from 'components/ui/SkeletonSummary.vue';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
onMounted(() => fetch());
const entity = ref(); const entity = ref();
const props = defineProps({ const props = defineProps({
url: { url: {
@ -16,14 +15,25 @@ const props = defineProps({
type: Object, type: Object,
default: null, default: null,
}, },
entityId: {
type: Number,
default: null,
},
}); });
const emit = defineEmits(['onFetch']); const emit = defineEmits(['onFetch']);
const route = useRoute();
const isSummary = ref();
defineExpose({ defineExpose({
entity, entity,
fetch, fetch,
}); });
onMounted(() => {
isSummary.value = String(route.path).endsWith('/summary');
fetch();
});
async function fetch() { async function fetch() {
const params = {}; const params = {};
@ -48,7 +58,17 @@ watch(props, async () => {
<template v-if="entity"> <template v-if="entity">
<div class="summaryHeader bg-primary q-pa-sm text-weight-bolder"> <div class="summaryHeader bg-primary q-pa-sm text-weight-bolder">
<slot name="header-left"> <slot name="header-left">
<span></span> <router-link
v-if="!isSummary && route.meta.moduleName"
class="header link"
:to="{
name: `${route.meta.moduleName}Summary`,
params: { id: entityId || entity.id },
}"
>
<QIcon name="open_in_new" color="white" size="sm" />
</router-link>
<span v-else></span>
</slot> </slot>
<slot name="header" :entity="entity"> <slot name="header" :entity="entity">
<VnLv :label="`${entity.id} -`" :value="entity.name" /> <VnLv :label="`${entity.id} -`" :value="entity.name" />

View File

@ -10,8 +10,8 @@ const $props = defineProps({
size: { type: String, default: null }, size: { type: String, default: null },
title: { type: String, default: null }, title: { type: String, default: null },
}); });
const session = useSession(); const { getTokenMultimedia } = useSession();
const token = session.getToken(); const token = getTokenMultimedia();
const { t } = useI18n(); const { t } = useI18n();
const title = computed(() => $props.title ?? t('globals.system')); const title = computed(() => $props.title ?? t('globals.system'));

View File

@ -44,7 +44,7 @@ const props = defineProps({
}, },
offset: { offset: {
type: Number, type: Number,
default: 500, default: 0,
}, },
skeleton: { skeleton: {
type: Boolean, type: Boolean,
@ -54,6 +54,10 @@ const props = defineProps({
type: Function, type: Function,
default: null, default: null,
}, },
disableInfiniteScroll: {
type: Boolean,
default: false,
},
}); });
const emit = defineEmits(['onFetch', 'onPaginate']); const emit = defineEmits(['onFetch', 'onPaginate']);
@ -122,10 +126,11 @@ async function paginate() {
emit('onPaginate'); emit('onPaginate');
} }
async function onLoad(...params) { async function onLoad(index, done) {
if (!store.data) return; if (!store.data) {
return done();
}
const done = params[1];
if (store.data.length === 0 || !props.url) return done(false); if (store.data.length === 0 || !props.url) return done(false);
pagination.value.page = pagination.value.page + 1; pagination.value.page = pagination.value.page + 1;
@ -174,7 +179,8 @@ async function onLoad(...params) {
v-if="store.data" v-if="store.data"
@load="onLoad" @load="onLoad"
:offset="offset" :offset="offset"
class="full-width full-height" :disable="disableInfiniteScroll || !arrayData.hasMoreData.value"
class="full-width"
v-bind="$attrs" v-bind="$attrs"
> >
<slot name="body" :rows="store.data"></slot> <slot name="body" :rows="store.data"></slot>

View File

@ -118,7 +118,12 @@ async function search() {
autofocus autofocus
> >
<template #prepend> <template #prepend>
<QIcon name="search" v-if="!quasar.platform.is.mobile" /> <QIcon
v-if="!quasar.platform.is.mobile"
class="cursor-pointer"
name="search"
@click="search"
/>
</template> </template>
<template #append> <template #append>
<QIcon <QIcon

View File

@ -1,8 +1,8 @@
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import { getUrl } from './getUrl'; import { getUrl } from './getUrl';
const session = useSession(); const {getTokenMultimedia} = useSession();
const token = session.getToken(); const token = getTokenMultimedia();
export async function downloadFile(dmsId) { export async function downloadFile(dmsId) {
let appUrl = await getUrl('', 'lilium'); let appUrl = await getUrl('', 'lilium');

View File

@ -1,30 +1,56 @@
import { useState } from './useState'; import { useState } from './useState';
import { useRole } from './useRole'; import { useRole } from './useRole';
import { useUserConfig } from './useUserConfig'; import { useUserConfig } from './useUserConfig';
import axios from 'axios';
import useNotify from './useNotify';
export function useSession() { export function useSession() {
const { notify } = useNotify();
function getToken() { function getToken() {
const localToken = localStorage.getItem('token'); const localToken = localStorage.getItem('token');
const sessionToken = sessionStorage.getItem('token'); const sessionToken = sessionStorage.getItem('token');
return localToken || sessionToken || ''; return localToken || sessionToken || '';
} }
function getTokenMultimedia() {
const localTokenMultimedia = localStorage.getItem('tokenMultimedia');
const sessionTokenMultimedia = sessionStorage.getItem('tokenMultimedia');
return localTokenMultimedia || sessionTokenMultimedia || '';
}
function setToken(data) { function setToken(data) {
if (data.keepLogin) { if (data.keepLogin) {
localStorage.setItem('token', data.token); localStorage.setItem('token', data.token);
localStorage.setItem('tokenMultimedia', data.tokenMultimedia);
} else { } else {
sessionStorage.setItem('token', data.token); sessionStorage.setItem('token', data.token);
sessionStorage.setItem('tokenMultimedia', data.tokenMultimedia);
} }
} }
async function destroyToken(url, storage, key) {
function destroy() { if (storage.getItem(key)) {
if (localStorage.getItem('token')) try {
localStorage.removeItem('token') await axios.post(url, null, {
headers: { Authorization: storage.getItem(key) },
if (sessionStorage.getItem('token')) });
sessionStorage.removeItem('token'); } catch (error) {
notify('errors.statusUnauthorized', 'negative');
} finally {
storage.removeItem(key);
}
}
}
async function destroy() {
const tokens = {
tokenMultimedia: 'Accounts/logout',
token: 'VnUsers/logout',
};
for (const [key, url] of Object.entries(tokens)) {
await destroyToken(url, localStorage, key);
await destroyToken(url, sessionStorage, key);
}
const { setUser } = useState(); const { setUser } = useState();
@ -37,8 +63,8 @@ export function useSession() {
}); });
} }
async function login(token, keepLogin) { async function login(token, tokenMultimedia, keepLogin) {
setToken({ token, keepLogin }); setToken({ token, tokenMultimedia, keepLogin });
await useRole().fetch(); await useRole().fetch();
await useUserConfig().fetch(); await useUserConfig().fetch();
@ -53,6 +79,7 @@ export function useSession() {
return { return {
getToken, getToken,
getTokenMultimedia,
setToken, setToken,
destroy, destroy,
login, login,

View File

@ -0,0 +1,23 @@
import VnConfirm from 'components/ui/VnConfirm.vue';
import { useQuasar } from 'quasar';
export function useVnConfirm() {
const quasar = useQuasar();
const openConfirmationModal = (title, message, promise, successFn) => {
quasar
.dialog({
component: VnConfirm,
componentProps: {
title: title,
message: message,
promise: promise,
},
})
.onOk(async () => {
if (successFn) successFn();
});
};
return { openConfirmationModal };
}

View File

@ -97,6 +97,15 @@ select:-webkit-autofill {
background-color: var(--vn-light-gray); background-color: var(--vn-light-gray);
} }
.fill-icon {
font-variation-settings: 'FILL' 1;
}
.vn-table-separation-row {
height: 16px !important;
background-color: var(--vn-gray) !important;
}
/* Estilo para el asterisco en campos requeridos */ /* Estilo para el asterisco en campos requeridos */
.q-field.required .q-field__label:after { .q-field.required .q-field__label:after {
content: ' *'; content: ' *';

View File

@ -31,6 +31,7 @@ export default {
close: 'Close', close: 'Close',
cancel: 'Cancel', cancel: 'Cancel',
confirm: 'Confirm', confirm: 'Confirm',
assign: 'Assign',
back: 'Back', back: 'Back',
yes: 'Yes', yes: 'Yes',
no: 'No', no: 'No',
@ -73,6 +74,7 @@ export default {
company: 'Company', company: 'Company',
fieldRequired: 'Field required', fieldRequired: 'Field required',
allowedFilesText: 'Allowed file types: { allowedContentTypes }', allowedFilesText: 'Allowed file types: { allowedContentTypes }',
smsSent: 'SMS sent',
confirmDeletion: 'Confirm deletion', confirmDeletion: 'Confirm deletion',
confirmDeletionMessage: 'Are you sure you want to delete this?', confirmDeletionMessage: 'Are you sure you want to delete this?',
description: 'Description', description: 'Description',
@ -590,6 +592,7 @@ export default {
company: 'Company', company: 'Company',
dued: 'Due date', dued: 'Due date',
shortDued: 'Due date', shortDued: 'Due date',
amount: 'Amount',
}, },
card: { card: {
issued: 'Issued', issued: 'Issued',
@ -623,7 +626,7 @@ export default {
fillDates: 'Invoice date and the max date should be filled', fillDates: 'Invoice date and the max date should be filled',
invoiceDateLessThanMaxDate: 'Invoice date can not be less than max date', invoiceDateLessThanMaxDate: 'Invoice date can not be less than max date',
invoiceWithFutureDate: 'Exists an invoice with a future date', invoiceWithFutureDate: 'Exists an invoice with a future date',
noTicketsToInvoice: 'There are not clients to invoice', noTicketsToInvoice: 'There are not tickets to invoice',
criticalInvoiceError: 'Critical invoicing error, process stopped', criticalInvoiceError: 'Critical invoicing error, process stopped',
}, },
table: { table: {
@ -833,6 +836,7 @@ export default {
notifications: 'Notifications', notifications: 'Notifications',
workerCreate: 'New worker', workerCreate: 'New worker',
department: 'Department', department: 'Department',
pda: 'PDA',
}, },
list: { list: {
name: 'Name', name: 'Name',
@ -874,6 +878,13 @@ export default {
subscribed: 'Subscribed to the notification', subscribed: 'Subscribed to the notification',
unsubscribed: 'Unsubscribed from the notification', unsubscribed: 'Unsubscribed from the notification',
}, },
pda: {
newPDA: 'New PDA',
currentPDA: 'Current PDA',
model: 'Model',
serialNumber: 'Serial number',
removePDA: 'Deallocate PDA',
},
create: { create: {
name: 'Name', name: 'Name',
lastName: 'Last name', lastName: 'Last name',
@ -939,6 +950,22 @@ export default {
uncompleteTrays: 'There are incomplete trays', uncompleteTrays: 'There are incomplete trays',
}, },
}, },
'route/roadmap': {
pageTitles: {
roadmap: 'Roadmap',
summary: 'Summary',
basicData: 'Basic Data',
stops: 'Stops'
},
},
roadmap: {
pageTitles: {
roadmap: 'Roadmap',
summary: 'Summary',
basicData: 'Basic Data',
stops: 'Stops'
},
},
route: { route: {
pageTitles: { pageTitles: {
routes: 'Routes', routes: 'Routes',
@ -947,6 +974,11 @@ export default {
create: 'Create', create: 'Create',
basicData: 'Basic Data', basicData: 'Basic Data',
summary: 'Summary', summary: 'Summary',
RouteRoadmap: 'Roadmaps',
RouteRoadmapCreate: 'Create roadmap',
tickets: 'Tickets',
log: 'Log',
autonomous: 'Autonomous',
}, },
cmr: { cmr: {
list: { list: {

View File

@ -31,6 +31,7 @@ export default {
close: 'Cerrar', close: 'Cerrar',
cancel: 'Cancelar', cancel: 'Cancelar',
confirm: 'Confirmar', confirm: 'Confirmar',
assign: 'Asignar',
back: 'Volver', back: 'Volver',
yes: 'Si', yes: 'Si',
no: 'No', no: 'No',
@ -73,6 +74,7 @@ export default {
company: 'Empresa', company: 'Empresa',
fieldRequired: 'Campo requerido', fieldRequired: 'Campo requerido',
allowedFilesText: 'Tipos de archivo permitidos: { allowedContentTypes }', allowedFilesText: 'Tipos de archivo permitidos: { allowedContentTypes }',
smsSent: 'SMS enviado',
confirmDeletion: 'Confirmar eliminación', confirmDeletion: 'Confirmar eliminación',
confirmDeletionMessage: '¿Seguro que quieres eliminar?', confirmDeletionMessage: '¿Seguro que quieres eliminar?',
description: 'Descripción', description: 'Descripción',
@ -309,8 +311,8 @@ export default {
reference: 'Referencia', reference: 'Referencia',
invoiceNumber: 'Núm. factura', invoiceNumber: 'Núm. factura',
ordered: 'Pedida', ordered: 'Pedida',
confirmed: 'Confirmado', confirmed: 'Confirmada',
booked: 'Asentado', booked: 'Contabilizada',
raid: 'Redada', raid: 'Redada',
excludedFromAvailable: 'Inventario', excludedFromAvailable: 'Inventario',
travelReference: 'Referencia', travelReference: 'Referencia',
@ -590,6 +592,7 @@ export default {
company: 'Empresa', company: 'Empresa',
dued: 'Fecha vencimineto', dued: 'Fecha vencimineto',
shortDued: 'F. vencimiento', shortDued: 'F. vencimiento',
amount: 'Importe',
}, },
card: { card: {
issued: 'Fecha emisión', issued: 'Fecha emisión',
@ -625,7 +628,7 @@ export default {
invoiceDateLessThanMaxDate: invoiceDateLessThanMaxDate:
'La fecha de la factura no puede ser menor que la fecha máxima', 'La fecha de la factura no puede ser menor que la fecha máxima',
invoiceWithFutureDate: 'Existe una factura con una fecha futura', invoiceWithFutureDate: 'Existe una factura con una fecha futura',
noTicketsToInvoice: 'No hay clientes para facturar', noTicketsToInvoice: 'No existen tickets para facturar',
criticalInvoiceError: 'Error crítico en la facturación, proceso detenido', criticalInvoiceError: 'Error crítico en la facturación, proceso detenido',
}, },
table: { table: {
@ -833,6 +836,7 @@ export default {
notifications: 'Notificaciones', notifications: 'Notificaciones',
workerCreate: 'Nuevo trabajador', workerCreate: 'Nuevo trabajador',
department: 'Departamentos', department: 'Departamentos',
pda: 'PDA',
}, },
list: { list: {
name: 'Nombre', name: 'Nombre',
@ -874,6 +878,13 @@ export default {
subscribed: 'Se ha suscrito a la notificación', subscribed: 'Se ha suscrito a la notificación',
unsubscribed: 'Se ha dado de baja de la notificación', unsubscribed: 'Se ha dado de baja de la notificación',
}, },
pda: {
newPDA: 'Nueva PDA',
currentPDA: 'PDA Actual',
model: 'Modelo',
serialNumber: 'Número de serie',
removePDA: 'Desasignar PDA',
},
create: { create: {
name: 'Nombre', name: 'Nombre',
lastName: 'Apellido', lastName: 'Apellido',
@ -939,6 +950,22 @@ export default {
uncompleteTrays: 'Hay bandejas sin completar', uncompleteTrays: 'Hay bandejas sin completar',
}, },
}, },
'route/roadmap': {
pageTitles: {
roadmap: 'Troncales',
summary: 'Resumen',
basicData: 'Datos básicos',
stops: 'Paradas'
},
},
roadmap: {
pageTitles: {
roadmap: 'Troncales',
summary: 'Resumen',
basicData: 'Datos básicos',
stops: 'Paradas'
},
},
route: { route: {
pageTitles: { pageTitles: {
routes: 'Rutas', routes: 'Rutas',
@ -946,7 +973,12 @@ export default {
RouteList: 'Listado', RouteList: 'Listado',
create: 'Crear', create: 'Crear',
basicData: 'Datos básicos', basicData: 'Datos básicos',
summary: 'Summary', summary: 'Resumen',
RouteRoadmap: 'Troncales',
RouteRoadmapCreate: 'Crear troncal',
tickets: 'Tickets',
log: 'Historial',
autonomous: 'Autónomos',
}, },
cmr: { cmr: {
list: { list: {

View File

@ -13,8 +13,8 @@ import { useSession } from 'src/composables/useSession';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const session = useSession(); const { getTokenMultimedia } = useSession();
const token = session.getToken(); const token = getTokenMultimedia();
const claimFilter = { const claimFilter = {
fields: [ fields: [

View File

@ -11,8 +11,8 @@ import FetchData from 'components/FetchData.vue';
const router = useRouter(); const router = useRouter();
const quasar = useQuasar(); const quasar = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
const session = useSession(); const { getTokenMultimedia } = useSession();
const token = session.getToken(); const token = getTokenMultimedia();
const claimId = computed(() => router.currentRoute.value.params.id); const claimId = computed(() => router.currentRoute.value.params.id);

View File

@ -14,8 +14,8 @@ import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const session = useSession(); const { getTokenMultimedia } = useSession();
const token = session.getToken(); const token = getTokenMultimedia();
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -172,6 +172,7 @@ function openDialog(dmsId) {
<CardSummary <CardSummary
ref="summary" ref="summary"
:url="`Claims/${entityId}/getSummary`" :url="`Claims/${entityId}/getSummary`"
:entity-id="entityId"
@on-fetch="getClaimDms" @on-fetch="getClaimDms"
> >
<template #header="{ entity: { claim } }"> <template #header="{ entity: { claim } }">
@ -270,7 +271,7 @@ function openDialog(dmsId) {
> >
<ItemDescriptorProxy <ItemDescriptorProxy
v-if="col.name == 'description'" v-if="col.name == 'description'"
:id="props.row.id" :id="props.row.sale.itemFk"
:sale-fk="props.row.saleFk" :sale-fk="props.row.saleFk"
></ItemDescriptorProxy> ></ItemDescriptorProxy>
</QTh> </QTh>

View File

@ -11,8 +11,8 @@ import VnInput from 'src/components/common/VnInput.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const session = useSession(); const { getTokenMultimedia } = useSession();
const token = session.getToken(); const token = getTokenMultimedia();
const workers = ref([]); const workers = ref([]);
const workersCopy = ref([]); const workersCopy = ref([]);

View File

@ -61,6 +61,7 @@ const onFilterTravelSelected = (formData, id) => {
:url-update="`Entries/${route.params.id}`" :url-update="`Entries/${route.params.id}`"
model="entry" model="entry"
auto-load auto-load
:clear-store-on-unmount="false"
> >
<template #form="{ data }"> <template #form="{ data }">
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">

View File

@ -43,7 +43,7 @@ const tableColumnComponents = computed(() => ({
item: { item: {
component: QBtn, component: QBtn,
props: { props: {
color: 'blue', color: 'primary',
flat: true, flat: true,
}, },
event: () => ({}), event: () => ({}),
@ -54,6 +54,7 @@ const tableColumnComponents = computed(() => ({
type: 'number', type: 'number',
min: 0, min: 0,
class: 'input-number', class: 'input-number',
dense: true,
}, },
event: getInputEvents, event: getInputEvents,
}, },
@ -67,6 +68,7 @@ const tableColumnComponents = computed(() => ({
'use-input': true, 'use-input': true,
'hide-selected': true, 'hide-selected': true,
options: packagingsOptions.value, options: packagingsOptions.value,
dense: true,
}, },
event: getInputEvents, event: getInputEvents,
}, },
@ -76,6 +78,7 @@ const tableColumnComponents = computed(() => ({
type: 'number', type: 'number',
min: 0, min: 0,
class: 'input-number', class: 'input-number',
dense: true,
}, },
event: getInputEvents, event: getInputEvents,
}, },
@ -84,6 +87,7 @@ const tableColumnComponents = computed(() => ({
props: { props: {
type: 'number', type: 'number',
min: 0, min: 0,
dense: true,
}, },
event: getInputEvents, event: getInputEvents,
}, },
@ -92,6 +96,7 @@ const tableColumnComponents = computed(() => ({
props: { props: {
type: 'number', type: 'number',
min: 0, min: 0,
dense: true,
}, },
event: getInputEvents, event: getInputEvents,
}, },
@ -100,6 +105,7 @@ const tableColumnComponents = computed(() => ({
props: { props: {
type: 'number', type: 'number',
min: 0, min: 0,
dense: true,
}, },
event: getInputEvents, event: getInputEvents,
}, },
@ -108,6 +114,7 @@ const tableColumnComponents = computed(() => ({
props: { props: {
type: 'number', type: 'number',
min: 0, min: 0,
dense: true,
}, },
event: getInputEvents, event: getInputEvents,
}, },
@ -116,6 +123,7 @@ const tableColumnComponents = computed(() => ({
props: { props: {
type: 'number', type: 'number',
min: 0, min: 0,
dense: true,
}, },
event: getInputEvents, event: getInputEvents,
}, },
@ -124,6 +132,7 @@ const tableColumnComponents = computed(() => ({
props: { props: {
type: 'number', type: 'number',
min: 0, min: 0,
dense: true,
}, },
event: getInputEvents, event: getInputEvents,
}, },
@ -276,7 +285,7 @@ const toggleGroupingMode = async (buy, mode) => {
} }
}; };
const showLockIcon = (groupingMode, mode) => { const lockIconType = (groupingMode, mode) => {
if (mode === 'packing') { if (mode === 'packing') {
return groupingMode === 2 ? 'lock' : 'lock_open'; return groupingMode === 2 ? 'lock' : 'lock_open';
} else { } else {
@ -320,17 +329,21 @@ const showLockIcon = (groupingMode, mode) => {
:columns="entriesTableColumns" :columns="entriesTableColumns"
selection="multiple" selection="multiple"
row-key="id" row-key="id"
hide-bottom
class="full-width q-mt-md" class="full-width q-mt-md"
:grid="$q.screen.lt.md" :grid="$q.screen.lt.md"
v-model:selected="rowsSelected" v-model:selected="rowsSelected"
:no-data-label="t('globals.noResults')"
> >
<template #body="props"> <template #body="props">
<QTr> <QTr>
<QTd> <QTd>
<QCheckbox v-model="props.selected" /> <QCheckbox v-model="props.selected" />
</QTd> </QTd>
<QTd v-for="col in props.cols" :key="col.name"> <QTd
v-for="col in props.cols"
:key="col.name"
style="max-width: 100px"
>
<component <component
:is="tableColumnComponents[col.name].component" :is="tableColumnComponents[col.name].component"
v-bind="tableColumnComponents[col.name].props" v-bind="tableColumnComponents[col.name].props"
@ -350,7 +363,7 @@ const showLockIcon = (groupingMode, mode) => {
> >
<QBtn <QBtn
:icon=" :icon="
showLockIcon(props.row.groupingMode, col.name) lockIconType(props.row.groupingMode, col.name)
" "
@click="toggleGroupingMode(props.row, col.name)" @click="toggleGroupingMode(props.row, col.name)"
class="cursor-pointer" class="cursor-pointer"
@ -359,6 +372,16 @@ const showLockIcon = (groupingMode, mode) => {
dense dense
unelevated unelevated
push push
:style="{
'font-variation-settings': `'FILL' ${
lockIconType(
props.row.groupingMode,
col.name
) === 'lock'
? 1
: 0
}`,
}"
/> />
</template> </template>
<template <template
@ -397,7 +420,7 @@ const showLockIcon = (groupingMode, mode) => {
</QTr> </QTr>
<!-- Esta última row es utilizada para agregar un espaciado y así marcar una diferencia visual entre los diferentes buys --> <!-- Esta última row es utilizada para agregar un espaciado y así marcar una diferencia visual entre los diferentes buys -->
<QTr v-if="props.rowIndex !== rows.length - 1" class="separation-row"> <QTr v-if="props.rowIndex !== rows.length - 1" class="separation-row">
<QTd colspan="12" style="height: 24px" /> <QTd colspan="12" class="vn-table-separation-row" />
</QTr> </QTr>
</template> </template>
<template #item="props"> <template #item="props">
@ -448,9 +471,6 @@ const showLockIcon = (groupingMode, mode) => {
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.separation-row {
background-color: var(--vn-gray) !important;
}
.grid-style-transition { .grid-style-transition {
transition: transform 0.28s, background-color 0.28s; transition: transform 0.28s, background-color 0.28s;
} }

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref, computed } from 'vue'; import { ref, computed, watch } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@ -7,6 +7,7 @@ import CardDescriptor from 'components/ui/CardDescriptor.vue';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import useCardDescription from 'src/composables/useCardDescription'; import useCardDescription from 'src/composables/useCardDescription';
import { useState } from 'src/composables/useState';
import { toDate } from 'src/filters'; import { toDate } from 'src/filters';
import { usePrintService } from 'composables/usePrintService'; import { usePrintService } from 'composables/usePrintService';
@ -25,6 +26,8 @@ const $props = defineProps({
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const { openReport } = usePrintService(); const { openReport } = usePrintService();
const state = useState();
const entryDescriptorRef = ref(null);
const entryFilter = { const entryFilter = {
include: [ include: [
@ -71,6 +74,8 @@ const data = ref(useCardDescription());
const setData = (entity) => const setData = (entity) =>
(data.value = useCardDescription(entity.supplier.nickname, entity.id)); (data.value = useCardDescription(entity.supplier.nickname, entity.id));
const currentEntry = computed(() => state.get('entry'));
const getEntryRedirectionFilter = (entry) => { const getEntryRedirectionFilter = (entry) => {
let entryTravel = entry && entry.travel; let entryTravel = entry && entry.travel;
@ -95,17 +100,20 @@ const getEntryRedirectionFilter = (entry) => {
const showEntryReport = () => { const showEntryReport = () => {
openReport(`Entries/${route.params.id}/entry-order-pdf`); openReport(`Entries/${route.params.id}/entry-order-pdf`);
}; };
watch;
</script> </script>
<template> <template>
<CardDescriptor <CardDescriptor
ref="entryDescriptorRef"
module="Entry" module="Entry"
:url="`Entries/${entityId}`" :url="`Entries/${entityId}`"
:filter="entryFilter" :filter="entryFilter"
:title="data.title" :title="data.title"
:subtitle="data.subtitle" :subtitle="data.subtitle"
@on-fetch="setData" @on-fetch="setData"
data-key="entryData" data-key="entry"
> >
<template #menu="{ entity }"> <template #menu="{ entity }">
<QItem v-ripple clickable @click="showEntryReport(entity)"> <QItem v-ripple clickable @click="showEntryReport(entity)">
@ -126,17 +134,17 @@ const showEntryReport = () => {
:value="entity.travel?.warehouseOut?.name" :value="entity.travel?.warehouseOut?.name"
/> />
</template> </template>
<template #icons="{ entity }"> <template #icons>
<QCardActions class="q-gutter-x-md"> <QCardActions class="q-gutter-x-md">
<QIcon <QIcon
v-if="entity.isExcludedFromAvailable" v-if="currentEntry.isExcludedFromAvailable"
name="vn:inventory" name="vn:inventory"
color="primary" color="primary"
size="xs" size="xs"
> >
<QTooltip>{{ t('Inventory entry') }}</QTooltip> <QTooltip>{{ t('Inventory entry') }}</QTooltip>
</QIcon> </QIcon>
<QIcon v-if="entity.isRaid" name="vn:web" color="primary" size="xs"> <QIcon v-if="currentEntry.isRaid" name="vn:net" color="primary" size="xs">
<QTooltip>{{ t('Virtual entry') }}</QTooltip> <QTooltip>{{ t('Virtual entry') }}</QTooltip>
</QIcon> </QIcon>
</QCardActions> </QCardActions>

View File

@ -165,20 +165,27 @@ const fetchEntryBuys = async () => {
@on-fetch="(data) => setEntryData(data)" @on-fetch="(data) => setEntryData(data)"
> >
<template #header-left> <template #header-left>
<a class="header-link" :href="entryUrl"> <router-link
v-if="route.name !== 'EntrySummary'"
:to="{ name: 'EntrySummary', params: { id: entityId } }"
class="header link"
:href="entryUrl"
>
<QIcon name="open_in_new" color="white" size="sm" /> <QIcon name="open_in_new" color="white" size="sm" />
</a> </router-link>
</template> </template>
<template #header> <template #header>
<span>{{ entry.id }} - {{ entry.supplier.nickname }}</span> <span>{{ entry.id }} - {{ entry.supplier.nickname }}</span>
</template> </template>
<template #body> <template #body>
<QCard class="vn-one"> <QCard class="vn-one">
<a class="header header-link" :href="`#/entry/${entityId}/basic-data`"> <router-link
:to="{ name: 'EntryBasicData', params: { id: entityId } }"
class="header header-link"
>
{{ t('globals.summary.basicData') }} {{ t('globals.summary.basicData') }}
<QIcon name="open_in_new" /> <QIcon name="open_in_new" />
</a> </router-link>
<VnLv :label="t('entry.summary.commission')" :value="entry.commission" /> <VnLv :label="t('entry.summary.commission')" :value="entry.commission" />
@ -192,37 +199,15 @@ const fetchEntryBuys = async () => {
:label="t('entry.summary.invoiceNumber')" :label="t('entry.summary.invoiceNumber')"
:value="entry.invoiceNumber" :value="entry.invoiceNumber"
/> />
<QCheckbox
:label="t('entry.summary.ordered')"
v-model="entry.isOrdered"
:disable="true"
/>
<QCheckbox
:label="t('entry.summary.confirmed')"
v-model="entry.isConfirmed"
:disable="true"
/>
<QCheckbox
:label="t('entry.summary.booked')"
v-model="entry.isBooked"
:disable="true"
/>
<QCheckbox
:label="t('entry.summary.raid')"
v-model="entry.isRaid"
:disable="true"
/>
<QCheckbox
:label="t('entry.summary.excludedFromAvailable')"
v-model="entry.isExcludedFromAvailable"
:disable="true"
/>
</QCard> </QCard>
<QCard class="vn-one"> <QCard class="vn-one">
<a class="header header-link" :href="entryUrl"> <router-link
{{ t('Travel data') }} :to="{ name: 'EntryBasicData', params: { id: entityId } }"
class="header header-link"
>
{{ t('globals.summary.basicData') }}
<QIcon name="open_in_new" /> <QIcon name="open_in_new" />
</a> </router-link>
<VnLv :label="t('entry.summary.travelReference')"> <VnLv :label="t('entry.summary.travelReference')">
<template #value> <template #value>
@ -250,7 +235,7 @@ const fetchEntryBuys = async () => {
<QCheckbox <QCheckbox
:label="t('entry.summary.travelDelivered')" :label="t('entry.summary.travelDelivered')"
v-model="entry.isDelivered" v-model="entry.travel.isDelivered"
:disable="true" :disable="true"
/> />
<VnLv <VnLv
@ -265,7 +250,41 @@ const fetchEntryBuys = async () => {
<QCheckbox <QCheckbox
:label="t('entry.summary.travelReceived')" :label="t('entry.summary.travelReceived')"
v-model="entry.isReceived" v-model="entry.travel.isReceived"
:disable="true"
/>
</QCard>
<QCard class="vn-one">
<router-link
:to="{ name: 'TravelSummary', params: { id: entry.travel.id } }"
class="header header-link"
>
{{ t('Travel data') }}
<QIcon name="open_in_new" />
</router-link>
<QCheckbox
:label="t('entry.summary.ordered')"
v-model="entry.isOrdered"
:disable="true"
/>
<QCheckbox
:label="t('entry.summary.confirmed')"
v-model="entry.isConfirmed"
:disable="true"
/>
<QCheckbox
:label="t('entry.summary.booked')"
v-model="entry.isBooked"
:disable="true"
/>
<QCheckbox
:label="t('entry.summary.raid')"
v-model="entry.isRaid"
:disable="true"
/>
<QCheckbox
:label="t('entry.summary.excludedFromAvailable')"
v-model="entry.isExcludedFromAvailable"
:disable="true" :disable="true"
/> />
</QCard> </QCard>
@ -277,9 +296,9 @@ const fetchEntryBuys = async () => {
<QTable <QTable
:rows="entryBuys" :rows="entryBuys"
:columns="entriesTableColumns" :columns="entriesTableColumns"
hide-bottom
row-key="index" row-key="index"
class="full-width q-mt-md" class="full-width q-mt-md"
:no-data-label="t('globals.noResults')"
> >
<template #body="{ cols, row, rowIndex }"> <template #body="{ cols, row, rowIndex }">
<QTr no-hover> <QTr no-hover>
@ -325,11 +344,8 @@ const fetchEntryBuys = async () => {
</QTd> </QTd>
</QTr> </QTr>
<!-- Esta última row es utilizada para agregar un espaciado y así marcar una diferencia visual entre los diferentes buys --> <!-- Esta última row es utilizada para agregar un espaciado y así marcar una diferencia visual entre los diferentes buys -->
<QTr <QTr v-if="rowIndex !== entryBuys.length - 1">
v-if="rowIndex !== entryBuys.length - 1" <QTd colspan="10" class="vn-table-separation-row" />
class="separation-row"
>
<QTd colspan="10" style="height: 24px" />
</QTr> </QTr>
</template> </template>
</QTable> </QTable>
@ -338,13 +354,7 @@ const fetchEntryBuys = async () => {
</CardSummary> </CardSummary>
</template> </template>
<style lang="scss" scoped>
.separation-row {
background-color: var(--vn-gray) !important;
}
</style>
<i18n> <i18n>
es: es:
Travel data: 'Datos envío' Travel data: Datos envío
</i18n> </i18n>

View File

@ -20,8 +20,8 @@ import { dashIfEmpty } from 'src/filters';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
const router = useRouter(); const router = useRouter();
const session = useSession(); const { getTokenMultimedia } = useSession();
const token = session.getToken(); const token = getTokenMultimedia();
const stateStore = useStateStore(); const stateStore = useStateStore();
const { t } = useI18n(); const { t } = useI18n();

View File

@ -74,7 +74,7 @@ onMounted(async () => {
</QIcon> </QIcon>
<QIcon <QIcon
v-if="row.isRaid" v-if="row.isRaid"
name="vn:web" name="vn:net"
color="primary" color="primary"
size="xs" size="xs"
> >

View File

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

View File

@ -2,13 +2,15 @@
import { ref, computed } from 'vue'; 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 CardDescriptor from 'components/ui/CardDescriptor.vue'; import CardDescriptor from 'components/ui/CardDescriptor.vue';
import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy.vue'; import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy.vue';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import useCardDescription from 'src/composables/useCardDescription';
import InvoiceOutDescriptorMenu from './InvoiceOutDescriptorMenu.vue'; import InvoiceOutDescriptorMenu from './InvoiceOutDescriptorMenu.vue';
import useCardDescription from 'src/composables/useCardDescription';
import { toCurrency, toDate } from 'src/filters';
const $props = defineProps({ const $props = defineProps({
id: { id: {
type: Number, type: Number,
@ -23,7 +25,6 @@ const { t } = useI18n();
const entityId = computed(() => { const entityId = computed(() => {
return $props.id || route.params.id; return $props.id || route.params.id;
}); });
const descriptor = ref();
const filter = { const filter = {
include: [ include: [
@ -42,6 +43,8 @@ const filter = {
], ],
}; };
const descriptor = ref();
function ticketFilter(invoice) { function ticketFilter(invoice) {
return JSON.stringify({ refFk: invoice.ref }); return JSON.stringify({ refFk: invoice.ref });
} }
@ -61,7 +64,7 @@ const setData = (entity) => (data.value = useCardDescription(entity.ref, entity.
data-key="invoiceOutData" data-key="invoiceOutData"
> >
<template #menu="{ entity }"> <template #menu="{ entity }">
<InvoiceOutDescriptorMenu :invoice-out="entity" /> <InvoiceOutDescriptorMenu :invoice-out-data="entity" />
</template> </template>
<template #body="{ entity }"> <template #body="{ entity }">
<VnLv :label="t('invoiceOut.card.issued')" :value="toDate(entity.issued)" /> <VnLv :label="t('invoiceOut.card.issued')" :value="toDate(entity.issued)" />

View File

@ -1,40 +1,260 @@
<script setup> <script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
import TransferInvoiceForm from 'src/components/TransferInvoiceForm.vue';
import SendEmailDialog from 'components/common/SendEmailDialog.vue';
import useNotify from 'src/composables/useNotify';
import { useSession } from 'src/composables/useSession';
import { usePrintService } from 'composables/usePrintService';
import { useVnConfirm } from 'composables/useVnConfirm';
import axios from 'axios';
const $props = defineProps({
invoiceOutData: {
type: Object,
default: () => {},
},
});
const { notify } = useNotify();
const router = useRouter();
const session = useSession();
const token = session.getToken();
const { t } = useI18n(); const { t } = useI18n();
const { openReport, sendEmail } = usePrintService();
const { openConfirmationModal } = useVnConfirm();
const quasar = useQuasar();
const transferInvoiceDialogRef = ref();
const invoiceFormType = ref('pdf');
const defaultEmailAddress = ref($props.invoiceOutData.client?.email);
const showInvoicePdf = () => {
const url = `api/InvoiceOuts/${$props.invoiceOutData.id}/download?access_token=${token}`;
window.open(url, '_blank');
};
const showInvoiceCsv = () => {
openReport(`InvoiceOuts/${$props.invoiceOutData.ref}/invoice-csv`, {
recipientId: $props.invoiceOutData.client.id,
});
};
const showSendInvoiceDialog = (type) => {
invoiceFormType.value = type;
quasar.dialog({
component: SendEmailDialog,
componentProps: {
data: {
address: defaultEmailAddress.value,
},
promise: sendEmailInvoice,
},
});
};
const sendEmailInvoice = async ({ address }) => {
try {
if (!address) notify(`The email can't be empty`, 'negative');
if (invoiceFormType.value === 'pdf') {
return sendEmail(`InvoiceOuts/${$props.invoiceOutData.ref}/invoice-email`, {
recipientId: $props.invoiceOutData.client.id,
recipient: address,
});
} else {
return sendEmail(
`InvoiceOuts/${$props.invoiceOutData.ref}/invoice-csv-email`,
{
recipientId: $props.invoiceOutData.client.id,
recipient: address,
}
);
}
} catch (err) {
console.error('Error sending email', err);
}
};
const redirectToInvoiceOutList = () => {
router.push({ name: 'InvoiceOutList' });
};
const deleteInvoice = async () => {
try {
await axios.post(`InvoiceOuts/${$props.invoiceOutData.id}/delete`);
notify(t('InvoiceOut deleted'), 'positive');
} catch (err) {
console.error('Error deleting invoice out', err);
}
};
const bookInvoice = async () => {
try {
await axios.post(`InvoiceOuts/${$props.invoiceOutData.ref}/book`);
notify(t('InvoiceOut booked'), 'positive');
} catch (err) {
console.error('Error booking invoice out', err);
}
};
const generateInvoicePdf = async () => {
try {
await axios.post(`InvoiceOuts/${$props.invoiceOutData.id}/createPdf`);
notify(t('The invoice PDF document has been regenerated'), 'positive');
} catch (err) {
console.error('Error generating invoice out pdf', err);
}
};
const refundInvoice = async (withWarehouse) => {
try {
const params = { ref: $props.invoiceOutData.ref, withWarehouse: withWarehouse };
const { data } = await axios.post('InvoiceOuts/refund', params);
notify(
t('refundInvoiceSuccessMessage', {
refundTicket: data[0].id,
}),
'positive'
);
} catch (err) {
console.error('Error generating invoice out pdf', err);
}
};
</script> </script>
<template> <template>
<QItem v-ripple clickable> <QItem v-ripple clickable @click="transferInvoiceDialogRef.show()">
<QItemSection>{{ t('Transfer invoice to') }}</QItemSection> <QItemSection>{{ t('Transfer invoice to...') }}</QItemSection>
</QItem> </QItem>
<QItem v-ripple clickable> <QItem v-ripple clickable>
<QItemSection>{{ t('See invoice') }}</QItemSection> <QItemSection>{{ t('Show invoice...') }}</QItemSection>
<QItemSection side>
<QIcon name="keyboard_arrow_right" />
</QItemSection>
<QMenu anchor="top end" self="top start">
<QList>
<QItem v-ripple clickable @click="showInvoicePdf()">
<QItemSection>{{ t('As PDF') }}</QItemSection>
</QItem>
<QItem v-ripple clickable @click="showInvoiceCsv()">
<QItemSection>{{ t('As CSV') }}</QItemSection>
</QItem>
</QList>
</QMenu>
</QItem> </QItem>
<QItem v-ripple clickable> <QItem v-ripple clickable>
<QItemSection>{{ t('Send invoice') }}</QItemSection> <QItemSection>{{ t('Send invoice...') }}</QItemSection>
<QItemSection side>
<QIcon name="keyboard_arrow_right" />
</QItemSection>
<QMenu anchor="top end" self="top start">
<QList>
<QItem v-ripple clickable @click="showSendInvoiceDialog('pdf')">
<QItemSection>{{ t('Send PDF') }}</QItemSection>
</QItem> </QItem>
<QItem v-ripple clickable> <QItem v-ripple clickable @click="showSendInvoiceDialog('csv')">
<QItemSection>{{ t('Send CSV') }}</QItemSection>
</QItem>
</QList>
</QMenu>
</QItem>
<QItem
v-ripple
clickable
@click="
openConfirmationModal(
t('Confirm deletion'),
t('Are you sure you want to delete this invoice?'),
deleteInvoice,
redirectToInvoiceOutList
)
"
>
<QItemSection>{{ t('Delete invoice') }}</QItemSection> <QItemSection>{{ t('Delete invoice') }}</QItemSection>
</QItem> </QItem>
<QItem v-ripple clickable> <QItem
<QItemSection>{{ t('Post invoice') }}</QItemSection> v-ripple
clickable
@click="
openConfirmationModal(
'',
t('Are you sure you want to book this invoice?'),
bookInvoice
)
"
>
<QItemSection>{{ t('Book invoice') }}</QItemSection>
</QItem>
<QItem
v-ripple
clickable
@click="
openConfirmationModal(
t('Generate PDF invoice document'),
t('Are you sure you want to generate/regenerate the PDF invoice?'),
generateInvoicePdf
)
"
>
<QItemSection>{{ t('Generate PDF invoice') }}</QItemSection>
</QItem> </QItem>
<QItem v-ripple clickable> <QItem v-ripple clickable>
<QItemSection>{{ t('Regenerate invoice PDF') }}</QItemSection> <QItemSection>{{ t('Refund...') }}</QItemSection>
<QItemSection side>
<QIcon name="keyboard_arrow_right" />
</QItemSection>
<QMenu anchor="top end" self="top start">
<QList>
<QItem v-ripple clickable @click="refundInvoice(true)">
<QItemSection>{{ t('With warehouse') }}</QItemSection>
</QItem> </QItem>
<QItem v-ripple clickable> <QItem v-ripple clickable @click="refundInvoice(false)">
<QItemSection>{{ t('Pass') }}</QItemSection> <QItemSection>{{ t('Without warehouse') }}</QItemSection>
</QItem> </QItem>
</QList>
</QMenu>
<QTooltip>
{{ t('Create a single ticket with all the content of the current invoice') }}
</QTooltip>
</QItem>
<QDialog ref="transferInvoiceDialogRef">
<TransferInvoiceForm :invoice-out-data="invoiceOutData" />
</QDialog>
</template> </template>
<i18n> <i18n>
es: es:
Transfer invoice to: Transferir factura a Transfer invoice to...: Transferir factura a...
See invoice: Ver factura Show invoice...: Ver factura...
Send invoice: Enviar factura Send invoice...: Enviar factura...
Delete invoice: Eliminar factura Delete invoice: Eliminar factura
Post invoice: Asentar factura Book invoice: Asentar factura
Regenerate invoice PDF: Regenerar PDF factura Generate PDF invoice: Generar PDF factura
Pass: Abono Refund...: Abono
As PDF: como PDF
As CSV: como CSV
Send PDF: Enviar PDF
Send CSV: Enviar CSV
With warehouse: Con almacén
Without warehouse: Sin almacén
InvoiceOut deleted: Factura eliminada
Confirm deletion: Confirmar eliminación
Are you sure you want to delete this invoice?: Estas seguro de eliminar esta factura?
Are you sure you want to book this invoice?: Estas seguro de querer asentar esta factura?
InvoiceOut booked: Factura asentada
Generate PDF invoice document: Generar PDF de la factura
Are you sure you want to generate/regenerate the PDF invoice?: ¿Seguro que quieres generar/regenerar el PDF de la factura?
The invoice PDF document has been regenerated: El documento PDF de la factura ha sido regenerado
Create a single ticket with all the content of the current invoice: Crear un ticket único con todo el contenido de la factura actual
refundInvoiceSuccessMessage: Se ha creado el siguiente ticket de abono {refundTicket}
The email can't be empty: El email no puede estar vacío
en:
refundInvoiceSuccessMessage: The following refund ticket have been created {refundTicket}
</i18n> </i18n>

View File

@ -7,6 +7,8 @@ import { toCurrency, toDate } from 'src/filters';
import CardSummary from 'components/ui/CardSummary.vue'; import CardSummary from 'components/ui/CardSummary.vue';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import { getUrl } from 'src/composables/getUrl'; import { getUrl } from 'src/composables/getUrl';
import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue';
import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue';
onMounted(async () => { onMounted(async () => {
fetch(); fetch();
@ -67,35 +69,43 @@ const taxColumns = ref([
const ticketsColumns = ref([ const ticketsColumns = ref([
{ {
name: 'item', name: 'item',
label: 'invoiceOut.summary.ticketId', label: t('invoiceOut.summary.ticketId'),
field: (row) => row.id, field: (row) => row.id,
sortable: true, sortable: true,
align: 'left',
}, },
{ {
name: 'quantity', name: 'quantity',
label: 'invoiceOut.summary.nickname', label: t('invoiceOut.summary.nickname'),
field: (row) => row.nickname, field: (row) => row.nickname,
sortable: true, sortable: true,
align: 'left',
}, },
{ {
name: 'landed', name: 'landed',
label: 'invoiceOut.summary.shipped', label: t('invoiceOut.summary.shipped'),
field: (row) => row.shipped, field: (row) => row.shipped,
format: (value) => toDate(value), format: (value) => toDate(value),
sortable: true, sortable: true,
align: 'left',
}, },
{ {
name: 'landed', name: 'landed',
label: 'invoiceOut.summary.totalWithVat', label: t('invoiceOut.summary.totalWithVat'),
field: (row) => row.totalWithVat, field: (row) => row.totalWithVat,
format: (value) => toCurrency(value), format: (value) => toCurrency(value),
sortable: true, sortable: true,
align: 'left',
}, },
]); ]);
</script> </script>
<template> <template>
<CardSummary ref="summary" :url="`InvoiceOuts/${entityId}/summary`"> <CardSummary
ref="summary"
:url="`InvoiceOuts/${entityId}/summary`"
:entity-id="entityId"
>
<template #header="{ entity: { invoiceOut } }"> <template #header="{ entity: { invoiceOut } }">
<div>{{ invoiceOut.ref }} - {{ invoiceOut.client?.socialName }}</div> <div>{{ invoiceOut.ref }} - {{ invoiceOut.client?.socialName }}</div>
</template> </template>
@ -147,12 +157,21 @@ const ticketsColumns = ref([
<QIcon name="open_in_new" /> <QIcon name="open_in_new" />
</a> </a>
<QTable v-if="tickets" :columns="ticketsColumns" :rows="tickets" flat> <QTable v-if="tickets" :columns="ticketsColumns" :rows="tickets" flat>
<template #header="props"> <template #body-cell-item="{ value }">
<QTr :props="props"> <QTd>
<QTh v-for="col in props.cols" :key="col.name" :props="props"> <QBtn flat color="primary">
{{ t(col.label) }} {{ value }}
</QTh> <TicketDescriptorProxy :id="value" />
</QTr> </QBtn>
</QTd>
</template>
<template #body-cell-quantity="{ value, row }">
<QTd>
<QBtn flat color="primary" dense>
{{ value }}
<CustomerDescriptorProxy :id="row.id" />
</QBtn>
</QTd>
</template> </template>
</QTable> </QTable>
</QCard> </QCard>

View File

@ -21,6 +21,7 @@ const {
nPdfs, nPdfs,
totalPdfs, totalPdfs,
errors, errors,
addresses,
} = storeToRefs(invoiceOutGlobalStore); } = storeToRefs(invoiceOutGlobalStore);
const selectedCustomerId = ref(null); const selectedCustomerId = ref(null);
@ -86,6 +87,14 @@ const selectCustomerId = (id) => {
selectedCustomerId.value = id; selectedCustomerId.value = id;
}; };
const statusText = computed(() => {
return status.value === 'invoicing'
? `${t(`status.${status.value}`)} ${
addresses.value[getAddressNumber.value]?.clientId
}`
: t(`status.${status.value}`);
});
onMounted(() => (stateStore.rightDrawer = true)); onMounted(() => (stateStore.rightDrawer = true));
onUnmounted(() => { onUnmounted(() => {
stateStore.rightDrawer = false; stateStore.rightDrawer = false;
@ -103,7 +112,7 @@ onUnmounted(() => {
<QPage class="column items-center q-pa-md"> <QPage class="column items-center q-pa-md">
<QCard v-if="status" class="card"> <QCard v-if="status" class="card">
<QCardSection class="card-section"> <QCardSection class="card-section">
<span class="text">{{ t(`status.${status}`) }}</span> <span class="text">{{ statusText }}</span>
<span class="text">{{ <span class="text">{{
t('invoiceOut.globalInvoices.statusCard.percentageText', { t('invoiceOut.globalInvoices.statusCard.percentageText', {
getPercentage: getPercentage, getPercentage: getPercentage,

View File

@ -13,13 +13,8 @@ const { t } = useI18n();
const invoiceOutGlobalStore = useInvoiceOutGlobalStore(); const invoiceOutGlobalStore = useInvoiceOutGlobalStore();
// invoiceOutGlobalStore state and getters // invoiceOutGlobalStore state and getters
const { const { initialDataLoading, formInitialData, invoicing, status } =
initialDataLoading, storeToRefs(invoiceOutGlobalStore);
formInitialData,
invoicing,
status,
} = storeToRefs(invoiceOutGlobalStore);
// invoiceOutGlobalStore actions // invoiceOutGlobalStore actions
const { makeInvoice, setStatusValue } = invoiceOutGlobalStore; const { makeInvoice, setStatusValue } = invoiceOutGlobalStore;
@ -32,13 +27,7 @@ const printersOptions = ref([]);
const clientsOptions = ref([]); const clientsOptions = ref([]);
const formData = ref({ const formData = ref({});
companyFk: null,
invoiceDate: null,
maxShipped: null,
clientId: null,
printer: null,
});
const optionsInitialData = computed(() => { const optionsInitialData = computed(() => {
return ( return (

View File

@ -2,25 +2,32 @@
import { onMounted, onUnmounted, ref } from 'vue'; import { onMounted, onUnmounted, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { exportFile, useQuasar } from 'quasar';
import { useStateStore } from 'stores/useStateStore';
import VnPaginate from 'src/components/ui/VnPaginate.vue'; import VnPaginate from 'src/components/ui/VnPaginate.vue';
import InvoiceOutSummary from './Card/InvoiceOutSummary.vue'; import InvoiceOutSummary from './Card/InvoiceOutSummary.vue';
import { toDate, toCurrency } from 'src/filters/index';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import InvoiceOutFilter from './InvoiceOutFilter.vue';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import CardList from 'src/components/ui/CardList.vue'; import CardList from 'src/components/ui/CardList.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import CustomerDescriptorProxy from '../Customer/Card/CustomerDescriptorProxy.vue';
import CreateManualInvoiceForm from 'src/components/CreateManualInvoiceForm.vue';
import InvoiceOutFilter from './InvoiceOutFilter.vue';
import { toDate, toCurrency } from 'src/filters/index';
import { useStateStore } from 'stores/useStateStore';
import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { useSession } from 'src/composables/useSession';
const { t } = useI18n(); const { t } = useI18n();
const selectedCards = ref(new Map());
const quasar = useQuasar();
const router = useRouter(); const router = useRouter();
const stateStore = useStateStore(); const stateStore = useStateStore();
const session = useSession();
const token = session.getToken();
const { viewSummary } = useSummaryDialog(); const { viewSummary } = useSummaryDialog();
const manualInvoiceDialogRef = ref(null);
const selectedCards = ref(new Map());
onMounted(() => (stateStore.rightDrawer = true)); onMounted(() => (stateStore.rightDrawer = true));
onUnmounted(() => (stateStore.rightDrawer = false)); onUnmounted(() => (stateStore.rightDrawer = false));
@ -52,36 +59,36 @@ const toggleAllCards = (cardsData) => {
} }
}; };
const downloadCsv = () => { const openPdf = () => {
try {
if (selectedCards.value.size === 0) return; if (selectedCards.value.size === 0) return;
const selectedCardsArray = Array.from(selectedCards.value.values()); const selectedCardsArray = Array.from(selectedCards.value.values());
let file;
for (var i = 0; i < selectedCardsArray.length; i++) { if (selectedCards.value.size === 1) {
if (i == 0) file += Object.keys(selectedCardsArray[i]).join(';') + '\n'; const [invoiceOut] = selectedCardsArray;
file += const url = `api/InvoiceOuts/${invoiceOut.id}/download?access_token=${token}`;
Object.keys(selectedCardsArray[i]) window.open(url, '_blank');
.map(function (key) {
return selectedCardsArray[i][key];
})
.join(';') + '\n';
}
const status = exportFile('file.csv', file, {
encoding: 'windows-1252',
mimeType: 'text/csv;charset=windows-1252;',
});
if (status === true) {
quasar.notify({
message: t('fileAllowed'),
color: 'positive',
icon: 'check',
});
} else { } else {
quasar.notify({ const invoiceOutIdsArray = selectedCardsArray.map(
message: t('fileDenied'), (invoiceOut) => invoiceOut.id
color: 'negative', );
icon: 'warning', const invoiceOutIds = invoiceOutIdsArray.join(',');
const params = new URLSearchParams({
access_token: token,
ids: invoiceOutIds,
}); });
const url = `api/InvoiceOuts/downloadZip?${params}`;
window.open(url, '_blank');
} }
} catch (err) {
console.error('Error opening PDF');
}
};
const openCreateInvoiceModal = () => {
manualInvoiceDialogRef.value.show();
}; };
</script> </script>
@ -119,52 +126,21 @@ const downloadCsv = () => {
<VnPaginate <VnPaginate
auto-load auto-load
data-key="InvoiceOutList" data-key="InvoiceOutList"
order="issued DESC, id DESC" :order="['issued DESC', 'id DESC']"
url="InvoiceOuts/filter" url="InvoiceOuts/filter"
> >
<template #body="{ rows }"> <template #body="{ rows }">
<VnSubToolbar class="bg-vn-dark justify-end"> <VnSubToolbar class="bg-vn-dark justify-end">
<template #st-actions> <template #st-actions>
<QBtn <QBtn
@click="downloadCsv()" @click="openPdf()"
class="q-mr-xl" class="q-mr-md"
color="primary" color="primary"
icon="cloud_download"
:disable="selectedCards.size === 0" :disable="selectedCards.size === 0"
:label="t('globals.download')"
/>
<!-- <QBtnDropdown
class="q-mr-xl"
color="primary"
:disable="!manageCheckboxes && arrayElements.length < 1"
:label="t('globals.download')"
v-else
> >
<QList> <QTooltip>{{ t('downloadPdf') }}</QTooltip>
<QItem clickable v-close-popup @click="downloadCsv(rows)"> </QBtn>
<QItemSection>
<QItemLabel>
{{
t('globals.allRows', {
numberRows: rows.length,
})
}}
</QItemLabel>
</QItemSection>
</QItem>
<QItem clickable v-close-popup @click="downloadCsv(rows)">
<QItemSection>
<QItemLabel>
{{
t('globals.selectRows', {
numberRows: rows.length,
})
}}
</QItemLabel>
</QItemSection>
</QItem>
</QList>
</QBtnDropdown> -->
<QCheckbox <QCheckbox
left-label left-label
:label="t('globals.markAll')" :label="t('globals.markAll')"
@ -189,18 +165,23 @@ const downloadCsv = () => {
> >
<template #list-items> <template #list-items>
<VnLv <VnLv
:label="t('invoiceOut.list.shortIssued')" :label="t('invoiceOut.list.issued')"
:title-label="t('invoiceOut.list.issued')"
:value="toDate(row.issued)" :value="toDate(row.issued)"
/> />
<VnLv <VnLv
:label="t('invoiceOut.list.amount')" :label="t('invoiceOut.list.amount')"
:value="toCurrency(row.amount)" :value="toCurrency(row.amount)"
/> />
<VnLv <VnLv :label="t('invoiceOut.list.client')">
:label="t('invoiceOut.list.client')" <template #value>
:value="row.clientSocialName" <span class="link" @click.stop>
{{ row?.clientSocialName }}
<CustomerDescriptorProxy
:id="row?.clientFk"
/> />
</span>
</template>
</VnLv>
<VnLv <VnLv
:label="t('invoiceOut.list.shortCreated')" :label="t('invoiceOut.list.shortCreated')"
:title-label="t('invoiceOut.list.created')" :title-label="t('invoiceOut.list.created')"
@ -217,13 +198,6 @@ const downloadCsv = () => {
/> />
</template> </template>
<template #actions> <template #actions>
<QBtn
:label="t('components.smartCard.openCard')"
@click.stop="navigate(row.id)"
class="bg-vn-dark"
outline
type="reset"
/>
<QBtn <QBtn
:label="t('components.smartCard.openSummary')" :label="t('components.smartCard.openSummary')"
@click.stop="viewSummary(row.id, InvoiceOutSummary)" @click.stop="viewSummary(row.id, InvoiceOutSummary)"
@ -237,6 +211,20 @@ const downloadCsv = () => {
</div> </div>
</template> </template>
</VnPaginate> </VnPaginate>
<QPageSticky :offset="[20, 20]">
<QBtn fab icon="add" color="primary" @click="openCreateInvoiceModal()" />
<QTooltip>
{{ t('createInvoice') }}
</QTooltip>
</QPageSticky>
<QDialog
ref="manualInvoiceDialogRef"
transition-show="scale"
transition-hide="scale"
>
<CreateManualInvoiceForm />
</QDialog>
</QPage> </QPage>
</template> </template>
@ -246,9 +234,13 @@ en:
fileDenied: Browser denied file download... fileDenied: Browser denied file download...
fileAllowed: Successful download of CSV file fileAllowed: Successful download of CSV file
youCanSearchByInvoiceReference: You can search by invoice reference youCanSearchByInvoiceReference: You can search by invoice reference
downloadPdf: Download PDF
createInvoice: Make invoice
es: es:
searchInvoice: Buscar factura emitida searchInvoice: Buscar factura emitida
fileDenied: El navegador denegó la descarga de archivos... fileDenied: El navegador denegó la descarga de archivos...
fileAllowed: Descarga exitosa de archivo CSV fileAllowed: Descarga exitosa de archivo CSV
youCanSearchByInvoiceReference: Puedes buscar por referencia de la factura youCanSearchByInvoiceReference: Puedes buscar por referencia de la factura
downloadPdf: Descargar PDF
createInvoice: Crear factura
</i18n> </i18n>

View File

@ -1,17 +1,17 @@
<script setup> <script setup>
import { ref, computed, onBeforeMount } from 'vue'; import { ref, computed, onBeforeMount, onMounted, nextTick } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { QCheckbox, QBtn } from 'quasar';
import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue'; import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue';
import InvoiceOutNegativeFilter from './InvoiceOutNegativeBasesFilter.vue'; import InvoiceOutNegativeFilter from './InvoiceOutNegativeBasesFilter.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue';
import { toCurrency } from 'src/filters'; import { toCurrency } from 'src/filters';
import { useInvoiceOutGlobalStore } from 'src/stores/invoiceOutGlobal.js'; import { useInvoiceOutGlobalStore } from 'src/stores/invoiceOutGlobal.js';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import VnUserLink from 'src/components/ui/VnUserLink.vue';
const invoiceOutGlobalStore = useInvoiceOutGlobalStore(); const invoiceOutGlobalStore = useInvoiceOutGlobalStore();
const stateStore = useStateStore(); const stateStore = useStateStore();
@ -45,78 +45,16 @@ onBeforeMount(async () => {
stateStore.rightDrawer = true; stateStore.rightDrawer = true;
}); });
const componentIsRendered = ref(false);
onMounted(() =>
nextTick(() => {
componentIsRendered.value = true;
})
);
const rows = computed(() => arrayData.value.store.data); const rows = computed(() => arrayData.value.store.data);
const selectedCustomerId = ref(0);
const selectedWorkerId = ref(0);
const tableColumnComponents = {
company: {
component: 'span',
props: () => {},
event: () => {},
},
country: {
component: 'span',
props: () => {},
event: () => {},
},
clientId: {
component: QBtn,
props: () => ({ flat: true, color: 'blue' }),
event: (prop) => selectCustomerId(prop.value),
},
client: {
component: 'span',
props: () => {},
event: () => {},
},
amount: {
component: 'span',
props: () => {},
event: () => {},
},
base: {
component: 'span',
props: () => {},
event: () => {},
},
ticketId: {
component: 'span',
props: () => {},
event: () => {},
},
active: {
component: QCheckbox,
props: (prop) => ({
disable: true,
'model-value': Boolean(prop.value),
}),
event: () => {},
},
hasToInvoice: {
component: QCheckbox,
props: (prop) => ({
disable: true,
'model-value': Boolean(prop.value),
}),
event: () => {},
},
verifiedData: {
component: QCheckbox,
props: (prop) => ({
disable: true,
'model-value': Boolean(prop.value),
}),
event: () => {},
},
comercial: {
component: QBtn,
props: () => ({ flat: true, color: 'blue' }),
event: (prop) => selectWorkerId(prop.row.comercialId),
},
};
const columns = computed(() => [ const columns = computed(() => [
{ {
label: t('invoiceOut.negativeBases.company'), label: t('invoiceOut.negativeBases.company'),
@ -205,20 +143,17 @@ const downloadCSV = async () => {
params params
); );
}; };
const selectCustomerId = (id) => {
selectedCustomerId.value = id;
};
const selectWorkerId = (id) => {
selectedWorkerId.value = id;
};
</script> </script>
<template> <template>
<template v-if="stateStore.isHeaderMounted()"> <template v-if="stateStore.isHeaderMounted()">
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()"> <Teleport
<QBtn color="primary" icon-right="archive" no-caps @click="downloadCSV()" /> to="#st-actions"
v-if="stateStore?.isSubToolbarShown() && componentIsRendered"
>
<QBtn color="primary" icon-right="download" no-caps @click="downloadCSV()">
<QTooltip>{{ t('Download as CSV') }}</QTooltip>
</QBtn>
</Teleport> </Teleport>
</template> </template>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above> <QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
@ -236,31 +171,37 @@ const selectWorkerId = (id) => {
:pagination="{ rowsPerPage: 0 }" :pagination="{ rowsPerPage: 0 }"
class="full-width q-mt-md" class="full-width q-mt-md"
> >
<template #body-cell="props"> <template #body-cell-clientId="{ row }">
<QTd :props="props"> <QTd>
<component <QBtn flat dense color="blue"> {{ row.clientId }}</QBtn>
:is="tableColumnComponents[props.col.name].component" <CustomerDescriptorProxy :id="row.clientId" />
class="col-content" </QTd>
v-bind="tableColumnComponents[props.col.name].props(props)"
@click="tableColumnComponents[props.col.name].event(props)"
>
<template
v-if="
props.col.name !== 'active' &&
props.col.name !== 'hasToInvoice' &&
props.col.name !== 'verifiedData'
"
>{{ props.value }}
</template> </template>
<CustomerDescriptorProxy <template #body-cell-ticketId="{ row }">
v-if="props.col.name === 'clientId'" <QTd>
:id="selectedCustomerId" <QBtn flat dense color="blue"> {{ row.ticketFk }}</QBtn>
/> <TicketDescriptorProxy :id="row.ticketFk" />
<VnUserLink </QTd>
v-if="props.col.name === 'comercial'" </template>
:worker-id="selectedWorkerId" <template #body-cell-comercial="{ row }">
/> <QTd>
</component> <QBtn flat dense color="blue">{{ row.comercialName }}</QBtn>
<WorkerDescriptorProxy :id="row.comercialId" />
</QTd>
</template>
<template #body-cell-active="{ row }">
<QTd>
<QCheckbox :model-value="!!row.isActive" disable />
</QTd>
</template>
<template #body-cell-hasToInvoice="{ row }">
<QTd>
<QCheckbox :model-value="!!row.hasToInvoice" disable />
</QTd>
</template>
<template #body-cell-verifiedData="{ row }">
<QTd>
<QCheckbox :model-value="!!row.isTaxDataChecked" disable />
</QTd> </QTd>
</template> </template>
</QTable> </QTable>
@ -274,4 +215,7 @@ const selectWorkerId = (id) => {
} }
</style> </style>
<i18n></i18n> <i18n>
es:
Download as CSV: Descargar como CSV
</i18n>

View File

@ -41,7 +41,7 @@ const quasar = useQuasar();
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
const { getToken } = useSession(); const { getTokenMultimedia } = useSession();
const state = useState(); const state = useState();
const user = state.getUser(); const user = state.getUser();
@ -79,7 +79,7 @@ onMounted(async () => {
}); });
const getItemAvatar = async () => { const getItemAvatar = async () => {
const token = getToken(); const token = getTokenMultimedia();
const timeStamp = `timestamp=${Date.now()}`; const timeStamp = `timestamp=${Date.now()}`;
image.value = `/api/Images/catalog/200x200/${entityId.value}/download?access_token=${token}&${timeStamp}`; image.value = `/api/Images/catalog/200x200/${entityId.value}/download?access_token=${token}&${timeStamp}`;
}; };

View File

@ -30,8 +30,15 @@ async function onSubmit() {
const { data } = await axios.post('Accounts/login', params); const { data } = await axios.post('Accounts/login', params);
if (!data) return; if (!data) return;
const {
data: { multimediaToken },
} = await axios.get('VnUsers/ShareToken', {
headers: { Authorization: data.token },
});
await session.login(data.token, keepLogin.value); if (!multimediaToken) return;
await session.login(data.token, multimediaToken.id, keepLogin.value);
quasar.notify({ quasar.notify({
message: t('login.loginSuccess'), message: t('login.loginSuccess'),

View File

@ -11,8 +11,8 @@ import toCurrency from '../../../filters/toCurrency';
const DEFAULT_PRICE_KG = 0; const DEFAULT_PRICE_KG = 0;
const session = useSession(); const { getTokenMultimedia } = useSession();
const token = session.getToken(); const token = getTokenMultimedia();
const { t } = useI18n(); const { t } = useI18n();
defineProps({ defineProps({

View File

@ -17,9 +17,9 @@ import axios from 'axios';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const session = useSession(); const { getTokenMultimedia } = useSession();
const quasar = useQuasar(); const quasar = useQuasar();
const token = session.getToken(); const token = getTokenMultimedia();
const orderSummary = ref({ const orderSummary = ref({
total: null, total: null,
vat: null, vat: null,

View File

@ -0,0 +1,237 @@
<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 agencyList = ref([]);
const agencyAgreementList = ref([]);
const supplierList = ref([]);
const exprBuilder = (param, value) => {
switch (param) {
case 'agencyModeFk':
return { 'a.agencyModeFk': value };
case 'supplierFk':
return { 'a.supplierName': value };
case 'routeFk':
return { 'a.routeFk': value };
case 'created':
case 'agencyFk':
case 'packages':
case 'm3':
case 'kmTotal':
case 'price':
case 'invoiceInFk':
return { [`a.${param}`]: value };
}
};
</script>
<template>
<FetchData
url="AgencyModes"
:filter="{ fields: ['id', 'name'] }"
sort-by="name ASC"
limit="30"
@on-fetch="(data) => (agencyList = data)"
auto-load
/>
<FetchData
url="Agencies"
:filter="{ fields: ['id', 'name'] }"
sort-by="name ASC"
limit="30"
@on-fetch="(data) => (agencyAgreementList = data)"
auto-load
/>
<FetchData
url="Suppliers"
:filter="{ fields: ['name'] }"
sort-by="name ASC"
limit="30"
@on-fetch="(data) => (supplierList = data)"
auto-load
/>
<VnFilterPanel
:data-key="props.dataKey"
:expr-builder="exprBuilder"
search-button
@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>
<VnInput v-model="params.routeFk" :label="t('ID')" is-outlined />
</QItemSection>
</QItem>
<QItem class="q-my-sm" v-if="agencyList">
<QItemSection>
<VnSelectFilter
:label="t('Agency route')"
v-model="params.agencyModeFk"
:options="agencyList"
option-value="id"
option-label="name"
dense
outlined
rounded
emit-value
map-options
use-input
is-clearable
:input-debounce="0"
/>
</QItemSection>
</QItem>
<QItem class="q-my-sm" v-if="agencyAgreementList">
<QItemSection>
<VnSelectFilter
:label="t('Agency agreement')"
v-model="params.agencyFk"
:options="agencyAgreementList"
option-value="id"
option-label="name"
dense
outlined
rounded
emit-value
map-options
use-input
is-clearable
:input-debounce="0"
/>
</QItemSection>
</QItem>
<QItem class="q-my-sm" v-if="supplierList">
<QItemSection>
<VnSelectFilter
:label="t('Autonomous')"
v-model="params.supplierFk"
:options="supplierList"
option-value="name"
option-label="name"
dense
outlined
rounded
emit-value
map-options
use-input
is-clearable
:input-debounce="0"
/>
</QItemSection>
</QItem>
<QItem class="q-my-sm">
<QItemSection>
<VnInputDate
v-model="params.created"
:label="t('Date')"
is-outlined
/>
</QItemSection>
</QItem>
<QItem class="q-my-sm">
<QItemSection>
<VnInputDate
v-model="params.from"
:label="t('From')"
is-outlined
/>
</QItemSection>
</QItem>
<QItem class="q-my-sm">
<QItemSection>
<VnInputDate
v-model="params.to"
:label="t('To')"
is-outlined
is-clearable
/>
</QItemSection>
</QItem>
<QItem class="q-my-sm">
<QItemSection>
<VnInput v-model="params.packages" :label="t('Packages')" is-outlined />
</QItemSection>
</QItem>
<QItem class="q-my-sm">
<QItemSection>
<VnInput v-model="params.m3" :label="t('m3')" is-outlined />
</QItemSection>
</QItem>
<QItem class="q-my-sm">
<QItemSection>
<VnInput v-model="params.kmTotal" :label="t('Km')" is-outlined />
</QItemSection>
</QItem>
<QItem class="q-my-sm">
<QItemSection>
<VnInput v-model="params.price" :label="t('Price')" is-outlined />
</QItemSection>
</QItem>
<QItem class="q-my-sm">
<QItemSection>
<VnInput v-model="params.invoiceInFk" :label="t('Received')" is-outlined />
</QItemSection>
</QItem>
</QList>
</template>
</VnFilterPanel>
</template>
<i18n>
en:
params:
agencyModeFk: Agency route
m3:
from: From
to: To
date: Date
agencyFk: Agency agreement
packages: Packages
price: Price
invoiceInFk: Received
supplierFk: Autonomous
es:
params:
agencyModeFk: Agencia ruta
m3:
from: Desde
to: Hasta
date: Fecha
agencyFk: Agencia Acuerdo
packages: Bultos
price: Precio
invoiceInFk: Recibida
supplierFk: Autónomos
From: Desde
To: Hasta
Date: Fecha
Agency route: Agencia Ruta
Agency agreement: Agencia Acuerdo
Packages: Bultos
Price: Precio
Received: Recibida
Autonomous: Autónomos
</i18n>

View File

@ -11,12 +11,15 @@ 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 VnInputTime from 'components/common/VnInputTime.vue'; import VnInputTime from 'components/common/VnInputTime.vue';
import RouteSearchbar from "pages/Route/Card/RouteSearchbar.vue";
import {useStateStore} from "stores/useStateStore";
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const shelvingId = route.params?.id || null; const stateStore = useStateStore();
const isNew = Boolean(!shelvingId); const shelvingId = ref(route.params?.id || null);
const isNew = Boolean(!shelvingId.value);
const defaultInitialData = { const defaultInitialData = {
agencyModeFk: null, agencyModeFk: null,
created: null, created: null,
@ -78,6 +81,11 @@ const onSave = (data, response) => {
</script> </script>
<template> <template>
<VnSubToolbar /> <VnSubToolbar />
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<RouteSearchbar />
</Teleport>
</template>
<FetchData <FetchData
url="Workers/search" url="Workers/search"
:filter="{ fields: ['id', 'nickname'] }" :filter="{ fields: ['id', 'nickname'] }"
@ -89,7 +97,7 @@ const onSave = (data, response) => {
<FetchData <FetchData
url="AgencyModes/isActive" url="AgencyModes/isActive"
:filter="{ fields: ['id', 'name'] }" :filter="{ fields: ['id', 'name'] }"
sort-by="name ASC" sort-by="name"
limit="30" limit="30"
@on-fetch="(data) => (agencyList = data)" @on-fetch="(data) => (agencyList = data)"
auto-load auto-load
@ -103,13 +111,13 @@ const onSave = (data, response) => {
auto-load auto-load
/> />
<FormModel <FormModel
:url="isNew ? null : `Routes/${shelvingId}`" :url="isNew ? null : `Routes/${route.params?.id}`"
:url-create="isNew ? 'Routes' : null" :url-create="isNew ? 'Routes' : null"
:observe-form-changes="!isNew" :observe-form-changes="!isNew"
:filter="routeFilter" :filter="routeFilter"
model="route" model="route"
:auto-load="!isNew" :auto-load="!isNew"
:form-initial-data="defaultInitialData" :form-initial-data="isNew ? defaultInitialData : null"
@on-data-saved="onSave" @on-data-saved="onSave"
> >
<template #form="{ data }"> <template #form="{ data }">
@ -131,7 +139,7 @@ const onSave = (data, response) => {
<QItemSection> <QItemSection>
<QItemLabel>{{ opt.name }}</QItemLabel> <QItemLabel>{{ opt.name }}</QItemLabel>
<QItemLabel caption> <QItemLabel caption>
{{ opt.nickname }},{{ opt.code }} {{ opt.nickname }}, {{ opt.code }}
</QItemLabel> </QItemLabel>
</QItemSection> </QItemSection>
</QItem> </QItem>
@ -212,6 +220,8 @@ const onSave = (data, response) => {
<VnInput <VnInput
v-model="data.description" v-model="data.description"
:label="t('Description')" :label="t('Description')"
type="textarea"
:rows="3"
clearable clearable
/> />
</div> </div>
@ -230,4 +240,5 @@ es:
Hour finished: Hora fin Hour finished: Hora fin
Description: Descripción Description: Descripción
Is served: Se ha servido Is served: Se ha servido
Created: Creado
</i18n> </i18n>

View File

@ -0,0 +1,241 @@
<script setup>
import { useDialogPluginComponent } from 'quasar';
import { useI18n } from 'vue-i18n';
import { ref } from 'vue';
import FetchData from 'components/FetchData.vue';
import axios from 'axios';
import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy.vue';
import TicketDescriptorProxy from 'pages/Ticket/Card/TicketDescriptorProxy.vue';
const { t } = useI18n();
const $props = defineProps({
id: {
type: Number,
required: true,
},
});
const emit = defineEmits([...useDialogPluginComponent.emits]);
const { dialogRef, onDialogHide } = useDialogPluginComponent();
const columns = ref([
{
name: 'ticket',
label: t('Ticket'),
field: (row) => row.id,
sortable: true,
align: 'left',
},
{
name: 'client',
label: t('Client'),
field: (row) => row.nickname,
sortable: true,
align: 'left',
},
{
name: 'province',
label: t('Province'),
field: (row) => row?.address?.province?.name,
sortable: true,
align: 'left',
},
{
name: 'city',
label: t('City'),
field: (row) => row?.address?.city,
sortable: true,
align: 'left',
},
{
name: 'pc',
label: t('PC'),
field: (row) => row?.address?.postalCode,
sortable: true,
align: 'left',
},
{
name: 'address',
label: t('Address'),
field: (row) => row?.address?.street,
sortable: true,
align: 'left',
},
{
name: 'zone',
label: t('Zone'),
field: (row) => row?.zone?.name,
sortable: true,
align: 'left',
},
{
name: 'actions',
label: '',
sortable: false,
align: 'right',
},
]);
const refreshKey = ref(0);
const selectedRows = ref([]);
const isLoading = ref(true);
const ticketList = ref([]);
const onDataFetched = (data) => {
ticketList.value = data;
isLoading.value = false;
};
const selectedTicket = ref(null);
const unlinkZone = async (ticket) => {
await axios.post('Routes/unlink', {
agencyModeId: ticket?.agencyModeFk,
zoneId: ticket.zoneFk,
});
selectedTicket.value = null;
refreshKey.value++;
};
const setTicketsRoute = async () => {
if (!selectedRows.value?.length) {
return;
}
await Promise.all(
(selectedRows.value || [])
.filter((ticket) => ticket?.id)
.map((ticket) =>
axios.patch(`Routes/${$props.id}/insertTicket`, { ticketId: ticket.id })
)
);
await axios.post(`Routes/${$props.id}/updateVolume`);
emit('ok');
emit('hide');
};
</script>
<template>
<FetchData
:key="refreshKey"
:url="`Routes/${$props.id}/getSuggestedTickets`"
@on-fetch="onDataFetched"
auto-load
/>
<QDialog
:model-value="Boolean(selectedTicket)"
@update:model-value="(value) => (!value ? (selectedTicket = null) : null)"
class="confirmation-dialog"
>
<QCard style="min-width: 350px">
<QCardSection>
<p class="text-h6 q-ma-none">{{ t('Unlink selected zone?') }}</p>
</QCardSection>
<QCardActions align="right">
<QBtn
flat
:label="t('globals.cancel')"
v-close-popup
class="text-primary"
/>
<QBtn color="primary" v-close-popup @click="unlinkZone(selectedTicket)">
{{ t('Unlink') }}
</QBtn>
</QCardActions>
</QCard>
</QDialog>
<QDialog ref="dialogRef" @hide="onDialogHide">
<QCard class="full-width dialog">
<QCardSection class="row items-center q-pb-none">
<div class="text-h6">{{ t('Tickets to add') }}</div>
<QSpace />
<QBtn icon="close" flat round dense v-close-popup />
</QCardSection>
<QCardSection>
<QTable
v-model:selected="selectedRows"
:columns="columns"
:loading="isLoading"
:rows="ticketList"
flat
row-key="id"
selection="multiple"
:rows-per-page-options="[0]"
hide-pagination
>
<template #body-cell-ticket="props">
<QTd :props="props">
<span class="link">
{{ props.value }}
<TicketDescriptorProxy :id="props?.row?.id" />
</span>
</QTd>
</template>
<template #body-cell-client="props">
<QTd :props="props">
<span class="link">
{{ props.value }}
<CustomerDescriptorProxy :id="props?.row?.clientFk" />
</span>
</QTd>
</template>
<template #body-cell-actions="props">
<QTd :props="props">
<QIcon
name="link_off"
size="xs"
color="primary"
class="cursor-pointer"
@click="selectedTicket = props?.row"
>
<QTooltip
>{{
t('Unlink zone', {
zone: props?.row?.zone?.name,
agency: props?.row?.agencyMode?.name,
})
}}
</QTooltip>
</QIcon>
</QTd>
</template>
</QTable>
</QCardSection>
<QCardActions align="right">
<QBtn flat v-close-popup class="text-primary"
>{{ t('globals.cancel') }}
</QBtn>
<QBtn
color="primary"
:disable="!selectedRows?.length"
@click="setTicketsRoute"
>{{ t('globals.add') }}
</QBtn>
</QCardActions>
</QCard>
</QDialog>
</template>
<style lang="scss" scoped>
.confirmation-dialog {
z-index: 6001 !important;
}
.dialog {
max-width: 1280px;
}
</style>
<i18n>
en:
Unlink zone: Unlink zone {zone} from agency {agency}
es:
Tickets to add: Tickets a añadir
Client: Cliente
Province: Provincia
City: Población
PC: CP
Address: Dirección
Zone: Zona
Unlink zone: Desvincular zona {zone} de agencia {agency}
Unlink: Desvincular
</i18n>

View File

@ -9,6 +9,7 @@ const { t } = useI18n();
data-key="RouteList" data-key="RouteList"
:label="t('Search route')" :label="t('Search route')"
:info="t('You can search by route reference')" :info="t('You can search by route reference')"
custom-route-redirect-name="RouteList"
/> />
</template> </template>

View File

@ -8,9 +8,10 @@ import VnLv from 'components/ui/VnLv.vue';
import { QIcon } from 'quasar'; import { QIcon } from 'quasar';
import { dashIfEmpty, toCurrency, toDate, toHour } from 'src/filters'; import { dashIfEmpty, toCurrency, toDate, toHour } from 'src/filters';
import WorkerDescriptorProxy from 'pages/Worker/Card/WorkerDescriptorProxy.vue'; import WorkerDescriptorProxy from 'pages/Worker/Card/WorkerDescriptorProxy.vue';
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';
import RouteSearchbar from 'pages/Route/Card/RouteSearchbar.vue';
import { openBuscaman } from 'src/utils/buscaman';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -104,7 +105,7 @@ const ticketColumns = ref([
label: t('route.summary.ticket'), label: t('route.summary.ticket'),
field: (row) => row?.id, field: (row) => row?.id,
sortable: false, sortable: false,
align: 'right', align: 'center',
}, },
{ {
name: 'observations', name: 'observations',
@ -114,30 +115,20 @@ const ticketColumns = ref([
align: 'left', 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> </script>
<template> <template>
<div class="q-pa-md"> <template v-if="stateStore.isHeaderMounted()">
<CardSummary ref="summary" :url="`Routes/${entityId}/summary`"> <Teleport to="#searchbar">
<template #header-left> <RouteSearchbar />
<RouterLink :to="{ name: `RouteSummary`, params: { id: entityId } }"> </Teleport>
<QIcon name="open_in_new" color="white" size="sm" />
</RouterLink>
</template> </template>
<div class="q-pa-md full-width">
<CardSummary
ref="summary"
:url="`Routes/${entityId}/summary`"
:entity-id="entityId"
>
<template #header="{ entity }"> <template #header="{ entity }">
<span>{{ `${entity?.route.id} - ${entity?.route?.description}` }}</span> <span>{{ `${entity?.route.id} - ${entity?.route?.description}` }}</span>
</template> </template>
@ -207,9 +198,10 @@ const openBuscaman = async (route, ticket) => {
</QCard> </QCard>
<QCard class="vn-max"> <QCard class="vn-max">
<div class="header"> <a class="header" :href="`#/route/${entityId}/tickets`">
{{ t('route.summary.tickets') }} {{ t('route.summary.tickets') }}
</div> <QIcon name="open_in_new" color="primary" />
</a>
<QTable <QTable
:columns="ticketColumns" :columns="ticketColumns"
:rows="entity?.tickets" :rows="entity?.tickets"
@ -222,7 +214,7 @@ const openBuscaman = async (route, ticket) => {
<QTd auto-width> <QTd auto-width>
<span <span
class="text-primary cursor-pointer" class="text-primary cursor-pointer"
@click="openBuscaman(entity?.route, row)" @click="openBuscaman(entity?.route?.vehicleFk, [row])"
> >
{{ value }} {{ value }}
</span> </span>
@ -237,7 +229,7 @@ const openBuscaman = async (route, ticket) => {
</QTd> </QTd>
</template> </template>
<template #body-cell-ticket="{ value, row }"> <template #body-cell-ticket="{ value, row }">
<QTd auto-width> <QTd auto-width class="text-center">
<span class="text-primary cursor-pointer"> <span class="text-primary cursor-pointer">
{{ value }} {{ value }}
<TicketDescriptorProxy :id="row?.id" /> <TicketDescriptorProxy :id="row?.id" />

View File

@ -12,8 +12,8 @@ import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy
const stateStore = useStateStore(); const stateStore = useStateStore();
const { t } = useI18n(); const { t } = useI18n();
const session = useSession(); const { getTokenMultimedia } = useSession();
const token = session.getToken(); const token = getTokenMultimedia();
const selected = ref([]); const selected = ref([]);
const columns = computed(() => [ const columns = computed(() => [
@ -91,6 +91,24 @@ function downloadPdfs() {
} }
</script> </script>
<template> <template>
<template v-if="stateStore.isHeaderMounted()">
<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>
<div class="column items-center"> <div class="column items-center">
<div class="list"> <div class="list">
<VnPaginate <VnPaginate

View File

@ -0,0 +1,88 @@
<script setup>
import { useDialogPluginComponent } from 'quasar';
import { useI18n } from 'vue-i18n';
import { reactive, ref } from 'vue';
import axios from 'axios';
import FetchData from 'components/FetchData.vue';
import RoadmapAddStopForm from 'pages/Route/Roadmap/RoadmapAddStopForm.vue';
const emit = defineEmits([...useDialogPluginComponent.emits]);
const { dialogRef, onDialogHide } = useDialogPluginComponent();
const { t } = useI18n();
const props = defineProps({
roadmapFk: {
type: Number,
required: true,
},
});
const isLoading = ref(false);
const roadmapStopForm = reactive({
roadmapFk: Number(props.roadmapFk) || 0,
warehouseFk: null,
eta: new Date().toISOString(),
description: '',
});
const updateDefaultStop = (data) => {
const eta = new Date(data.etd);
eta.setDate(eta.getDate() + 1);
roadmapStopForm.eta = eta.toISOString();
};
const onSave = async () => {
isLoading.value = true;
try {
await axios.post('ExpeditionTrucks', { ...roadmapStopForm });
emit('ok');
} finally {
isLoading.value = false;
emit('hide');
}
};
</script>
<template>
<FetchData
:url="`Roadmaps/${roadmapFk}`"
auto-load
:filter="{ fields: ['etd'] }"
@on-fetch="updateDefaultStop"
/>
<QDialog ref="dialogRef" @hide="onDialogHide">
<QCard class="q-pa-lg">
<QCardSection class="row absolute absolute-top z-fullscreen">
<QSpace />
<QBtn icon="close" flat round dense v-close-popup />
</QCardSection>
<RoadmapAddStopForm
:roadmap-fk="roadmapFk"
:form-data="roadmapStopForm"
layout="dialog"
/>
<QCardActions align="right">
<QBtn :label="t('globals.cancel')" color="primary" flat v-close-popup />
<QBtn
:label="t('globals.confirm')"
color="primary"
:loading="isLoading"
@click="onSave"
unelevated
/>
</QCardActions>
</QCard>
</QDialog>
</template>
<style lang="scss" scoped>
.card-section {
gap: 16px;
}
</style>
<i18n>
es:
Warehouse: Almacén
ETA date: Fecha ETA
ETA time: Hora ETA
Description: Descripción
</i18n>

View File

@ -0,0 +1,104 @@
<script setup>
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnSelectFilter from 'components/common/VnSelectFilter.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
import VnInputTime from 'components/common/VnInputTime.vue';
import VnInput from 'components/common/VnInput.vue';
const props = defineProps({
formData: {
type: Object,
default: () => ({}),
},
roadmapFk: {
type: [Number, String],
required: true,
},
layout: {
type: String,
default: 'row',
},
});
const { t } = useI18n();
const warehouseList = ref([]);
const form = computed(() => props.formData);
const isDialog = computed(() => props.layout === 'dialog');
</script>
<template>
<FetchData
url="Warehouses"
auto-load
:filter="{ fields: ['id', 'name'] }"
sort-by="name"
limit="30"
@on-fetch="(data) => (warehouseList = data)"
/>
<div :class="[isDialog ? 'column' : 'form-gap', 'full-width flex']">
<QCardSection class="flex-grow q-px-none flex-1">
<VnSelectFilter
v-model.number="form.warehouseFk"
class="full-width"
:label="t('Warehouse')"
:options="warehouseList"
option-value="id"
option-label="name"
emit-value
map-options
use-input
hide-selected
autofocus
:input-debounce="0"
/>
</QCardSection>
<div :class="[!isDialog ? 'flex-2' : null, 'form-gap flex no-wrap']">
<QCardSection class="row q-px-none flex-1">
<VnInputDate
v-model="form.eta"
class="full-width"
:label="t('ETA date')"
/>
</QCardSection>
<QCardSection class="row q-px-none flex-1">
<VnInputTime
v-model="form.eta"
class="full-width"
:label="t('ETA hour')"
/>
</QCardSection>
</div>
<QCardSection class="q-px-none flex-1">
<VnInput
v-model="form.description"
class="full-width"
:label="t('Description')"
type="textarea"
:rows="1"
/>
</QCardSection>
</div>
</template>
<style lang="scss" scoped>
.form-gap {
gap: 16px;
}
.flex-1 {
flex: 1;
}
.flex-2 {
flex: 2;
}
</style>
<i18n>
es:
Warehouse: Almacén
ETA date: Fecha ETA
ETA hour: Hora ETA
Description: Descripción
Remove stop: Eliminar parada
Add stop: Añadir parada
</i18n>

View File

@ -0,0 +1,143 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import VnRow from 'components/ui/VnRow.vue';
import FormModel from 'components/FormModel.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
import VnInput from 'components/common/VnInput.vue';
import VnInputTime from 'components/common/VnInputTime.vue';
import VnSelectFilter from 'components/common/VnSelectFilter.vue';
import FetchData from 'components/FetchData.vue';
import {ref} from "vue";
const { t } = useI18n();
const router = useRouter();
const route = useRoute();
const supplierList = ref([]);
const filter = { include: [{ relation: 'supplier' }] };
const onSave = (data, response) => {
router.push({ name: 'RoadmapSummary', params: { id: response?.id } });
};
</script>
<template>
<VnSubToolbar />
<FetchData
url="Suppliers"
auto-load
:filter="{ fields: ['id', 'nickname'] }"
sort-by="nickname"
limit="30"
@on-fetch="(data) => (supplierList = data)"
/>
<FormModel
:url="`Roadmaps/${route.params?.id}`"
observe-form-changes
:filter="filter"
model="roadmap"
auto-load
@on-data-saved="onSave"
>
<template #form="{ data }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput v-model="data.name" :label="t('Roadmap')" clearable />
</div>
<div class="col">
<VnInputDate v-model="data.etd" :label="t('ETD date')" />
</div>
<div class="col">
<VnInputTime v-model="data.etd" :label="t('ETD hour')" />
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
v-model="data.tractorPlate"
:label="t('Tractor plate')"
clearable
/>
</div>
<div class="col">
<VnInput
v-model="data.trailerPlate"
:label="t('Trailer plate')"
clearable
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('Carrier')"
v-model="data.supplierFk"
:options="supplierList"
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.id }} - {{ opt.nickname }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</div>
<div class="col">
<VnInput
v-model="data.price"
:label="t('Price')"
type="number"
clearable
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
v-model="data.driverName"
:label="t('Driver name')"
clearable
/>
</div>
<div class="col">
<VnInput
v-model="data.phone"
:label="t('Phone')"
clearable
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
v-model="data.observations"
:label="t('Observations')"
clearable
/>
</div>
</VnRow>
</template>
</FormModel>
</template>
<i18n>
es:
Roadmap: Troncal
ETD date: Fecha ETD
ETD hour: Hora ETD
Tractor plate: Matrícula tractor
Trailer plate: Matrícula trailer
Carrier: Transportista
Price: Precio
Driver name: Nombre del conductor
Phone: Teléfono
Observations: Observaciones
</i18n>

View File

@ -0,0 +1,21 @@
<script setup>
import { useStateStore } from 'stores/useStateStore';
import LeftMenu from 'components/LeftMenu.vue';
import RoadmapDescriptor from "pages/Route/Roadmap/RoadmapDescriptor.vue";
const stateStore = useStateStore();
</script>
<template>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<QScrollArea class="fit">
<RoadmapDescriptor />
<QSeparator />
<LeftMenu source="card" />
</QScrollArea>
</QDrawer>
<QPageContainer>
<QPage>
<RouterView></RouterView>
</QPage>
</QPageContainer>
</template>

View File

@ -0,0 +1,56 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import VnRow from 'components/ui/VnRow.vue';
import FormModel from 'components/FormModel.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
import VnInput from 'components/common/VnInput.vue';
import VnInputTime from 'components/common/VnInputTime.vue';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const defaultInitialData = {
etd: Date.vnNew().toISOString(),
};
const onSave = (data, response) => {
router.push({ name: 'RoadmapSummary', params: { id: response?.id } });
};
</script>
<template>
<VnSubToolbar />
<FormModel
:url="null"
url-create="Roadmaps"
model="roadmap"
:observe-form-changes="false"
:auto-load="false"
:form-initial-data="defaultInitialData"
@on-data-saved="onSave"
>
<template #form="{ data }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
v-model="data.name"
:label="t('Roadmap')"
clearable
/>
</div>
<div class="col">
<VnInputDate v-model="data.etd" :label="t('ETD date')" />
</div>
<div class="col">
<VnInputTime v-model="data.etd" :label="t('ETD hour')" />
</div>
</VnRow>
</template>
</FormModel>
</template>
<i18n>
es:
Roadmap: Troncal
ETD date: Fecha ETD
ETD hour: Hora ETD
</i18n>

View File

@ -0,0 +1,63 @@
<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, toDateHour} from 'src/filters';
import SupplierDescriptorProxy from 'pages/Supplier/Card/SupplierDescriptorProxy.vue';
import RoadmapDescriptorMenu from "pages/Route/Roadmap/RoadmapDescriptorMenu.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 = { include: [{ relation: 'supplier' }] };
const data = ref(useCardDescription());
const setData = (entity) => (data.value = useCardDescription(entity.code, entity.id));
</script>
<template>
<CardDescriptor
module="Roadmap"
:url="`Roadmaps/${entityId}`"
:filter="filter"
:title="data.title"
:subtitle="data.subtitle"
data-key="Roadmap"
@on-fetch="setData"
>
<template #body="{ entity }">
<VnLv :label="t('Roadmap')" :value="entity?.name" />
<VnLv :label="t('ETD')" :value="toDateHour(entity?.etd)" />
<VnLv :label="t('Carrier')">
<template #value>
<span class="link" v-if="entity?.supplier?.id">
{{ dashIfEmpty(entity?.supplier?.nickname) }}
<SupplierDescriptorProxy :id="entity?.supplier?.id" />
</span>
</template>
</VnLv>
</template>
<template #menu="{ entity }">
<RoadmapDescriptorMenu :route="entity" />
</template>
</CardDescriptor>
</template>
<i18n>
es:
Roadmap: Troncal
Carrier: Transportista
</i18n>

View File

@ -0,0 +1,57 @@
<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('Confirm deletion'),
message: t('Are you sure you want to delete this roadmap?'),
promise: remove,
},
})
.onOk(async () => await router.push({ name: 'RouteRoadmap' }));
}
async function remove() {
if (!props.route.id) {
return;
}
await axios.delete(`Roadmaps/${props.route.id}`);
quasar.notify({
message: t('globals.dataDeleted'),
type: 'positive',
});
}
</script>
<template>
<QItem @click="confirmRemove" v-ripple clickable>
<QItemSection avatar>
<QIcon name="delete" />
</QItemSection>
<QItemSection>{{ t('Delete roadmap') }}</QItemSection>
</QItem>
</template>
<i18n>
es:
Confirm deletion: Confirmar eliminación
Are you sure you want to delete this roadmap?: ¿Estás seguro de que quieres eliminar esta troncal?
Delete roadmap: Eliminar troncal
</i18n>

View File

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

View File

@ -0,0 +1,183 @@
<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 supplierList = ref([]);
const exprBuilder = (param, value) => {
switch (param) {
case 'tractorPlate':
case 'trailerPlate':
case 'driverName':
case 'phone':
return { [param]: { like: `%${value}%` } };
case 'supplierFk':
case 'price':
return { [param]: value };
case 'from':
return { etd: { gte: value } };
case 'to':
return { etd: { lte: value } };
}
};
</script>
<template>
<FetchData
url="Suppliers"
:filter="{ fields: ['id', 'nickname'] }"
sort-by="nickname"
limit="30"
@on-fetch="(data) => (supplierList = data)"
auto-load
/>
<VnFilterPanel
:data-key="props.dataKey"
:search-button="true"
:expr-builder="exprBuilder"
@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 }">
<QItem class="q-my-sm">
<QItemSection>
<VnInputDate v-model="params.from" :label="t('From')" is-outlined />
</QItemSection>
</QItem>
<QItem class="q-my-sm">
<QItemSection>
<VnInputDate v-model="params.to" :label="t('To')" is-outlined />
</QItemSection>
</QItem>
<QItem class="q-my-sm">
<QItemSection>
<VnInput
v-model="params.tractorPlate"
:label="t('Tractor Plate')"
is-outlined
clearable
/>
</QItemSection>
</QItem>
<QItem class="q-my-sm">
<QItemSection>
<VnInput
v-model="params.trailerPlate"
:label="t('Trailer Plate')"
is-outlined
clearable
/>
</QItemSection>
</QItem>
<QItem v-if="supplierList" class="q-my-sm">
<QItemSection>
<VnSelectFilter
:label="t('Carrier')"
v-model="params.supplierFk"
:options="supplierList"
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.id }} - {{ opt.nickname }}</QItemLabel
>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</QItemSection>
</QItem>
<QItem class="q-my-sm">
<QItemSection>
<VnInput
v-model="params.price"
:label="t('Price')"
type="number"
is-outlined
clearable
/>
</QItemSection>
</QItem>
<QItem class="q-my-sm">
<QItemSection>
<VnInput
v-model="params.driverName"
:label="t('Driver name')"
is-outlined
clearable
/>
</QItemSection>
</QItem>
<QItem class="q-my-sm">
<QItemSection>
<VnInput
v-model="params.phone"
:label="t('Phone')"
is-outlined
clearable
/>
</QItemSection>
</QItem>
</template>
</VnFilterPanel>
</template>
<i18n>
en:
params:
tractorPlate: Tractor Plate
trailerPlate: Trailer Plate
supplierFk: Carrier
price: Price
driverName: Driver name
phone: Phone
from: From
to: To
es:
params:
tractorPlate: Matrícula del tractor
trailerPlate: Matrícula del trailer
supplierFk: Transportista
price: Precio
driverName: Nombre del conductor
phone: Teléfono
from: Desde
to: Hasta
From: Desde
To: Hasta
Tractor Plate: Matrícula del tractor
Trailer Plate: Matrícula del trailer
Carrier: Transportista
Price: Precio
Driver name: Nombre del conductor
Phone: Teléfono
</i18n>

View File

@ -0,0 +1,95 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import FetchData from 'components/FetchData.vue';
import { onMounted, ref } from 'vue';
import CrudModel from 'components/CrudModel.vue';
import RoadmapAddStopForm from "pages/Route/Roadmap/RoadmapAddStopForm.vue";
const { t } = useI18n();
const route = useRoute();
const roadmapStopsCrudRef = ref(null);
const defaultStop = ref({
roadmapFk: Number(route.params?.id) || 0,
eta: new Date().toISOString(),
});
const updateDefaultStop = (data) => {
const eta = new Date(data.etd);
eta.setDate(eta.getDate() + 1);
defaultStop.value.eta = eta.toISOString();
};
onMounted(() => {
if (roadmapStopsCrudRef.value) roadmapStopsCrudRef.value.reload();
});
</script>
<template>
<VnSubToolbar />
<FetchData
:url="`Roadmaps/${route.params?.id}`"
auto-load
:filter="{ fields: ['etd'] }"
@on-fetch="updateDefaultStop"
/>
<div class="q-pa-lg">
<CrudModel
ref="roadmapStopsCrudRef"
data-key="ExpeditionTrucks"
url="ExpeditionTrucks"
model="ExpeditionTrucks"
:filter="{ where: { roadmapFk: route.params?.id } }"
:default-remove="false"
:data-required="defaultStop"
>
<template #body="{ rows }">
<QCard class="q-pa-md">
<QCardSection
v-for="(row, index) in rows"
:key="index"
class="row no-wrap"
>
<RoadmapAddStopForm :roadmap-fk="route.params?.id" :form-data="row" />
<div class="col-1 row justify-center items-center">
<QIcon
name="delete"
size="sm"
class="cursor-pointer"
color="primary"
@click="roadmapStopsCrudRef.remove([row])"
>
<QTooltip>
{{ t('Remove stop') }}
</QTooltip>
</QIcon>
</div>
</QCardSection>
<QCardSection>
<QIcon
name="add"
size="sm"
class="cursor-pointer"
color="primary"
@click="roadmapStopsCrudRef.insert()"
>
<QTooltip>
{{ t('Add stop') }}
</QTooltip>
</QIcon>
</QCardSection>
</QCard>
</template>
</CrudModel>
</div>
</template>
<i18n>
es:
Warehouse: Almacén
ETA date: Fecha ETA
ETA hour: Hora ETA
Description: Descripción
Remove stop: Eliminar parada
Add stop: Añadir parada
</i18n>

View File

@ -0,0 +1,172 @@
<script setup>
import { computed, onMounted, onUnmounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { QIcon, useQuasar } from 'quasar';
import { dashIfEmpty, toDateHour } from 'src/filters';
import { useStateStore } from 'stores/useStateStore';
import VnLv from 'components/ui/VnLv.vue';
import CardSummary from 'components/ui/CardSummary.vue';
import SupplierDescriptorProxy from 'pages/Supplier/Card/SupplierDescriptorProxy.vue';
import VnLinkPhone from 'components/ui/VnLinkPhone.vue';
import RoadmapAddStopDialog from 'pages/Route/Roadmap/RoadmapAddStopDialog.vue';
const $props = defineProps({
id: {
type: Number,
default: 0,
},
});
const route = useRoute();
const stateStore = useStateStore();
const { t } = useI18n();
const quasar = useQuasar();
const summary = ref(null);
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 columns = ref([
{
name: 'warehouse',
label: t('Warehouse'),
field: (row) => dashIfEmpty(row?.warehouse?.name),
sortable: true,
align: 'left',
},
{
name: 'ETA',
label: t('ETA'),
field: (row) => toDateHour(row?.eta),
sortable: false,
align: 'left',
},
]);
const filter = {
include: [
{ relation: 'supplier' },
{ relation: 'worker' },
{
relation: 'expeditionTruck',
scope: { include: [{ relation: 'warehouse' }] },
},
],
};
const openAddStopDialog = () => {
quasar
.dialog({
component: RoadmapAddStopDialog,
componentProps: { roadmapFk: entityId.value },
})
.onOk(() => summary.value.fetch());
};
</script>
<template>
<div class="q-pa-md full-width">
<CardSummary ref="summary" :url="`Roadmaps/${entityId}`" :filter="filter">
<template #header-left>
<RouterLink :to="{ name: `RoadmapSummary`, params: { id: entityId } }">
<QIcon name="open_in_new" color="white" size="sm" />
</RouterLink>
</template>
<template #header="{ entity }">
<span>{{ `${entity?.id} - ${entity?.name}` }}</span>
</template>
<template #body="{ entity }">
<QCard class="vn-one">
<VnLv :label="t('Carrier')">
<template #value>
<span class="link" v-if="entity?.supplier?.id">
{{ dashIfEmpty(entity?.supplier?.nickname) }}
<SupplierDescriptorProxy :id="entity?.supplier?.id" />
</span>
</template>
</VnLv>
<VnLv :label="t('ETD')" :value="toDateHour(entity?.etd)" />
<VnLv
:label="t('Tractor Plate')"
:value="dashIfEmpty(entity?.tractorPlate)"
/>
<VnLv
:label="t('Trailer Plate')"
:value="dashIfEmpty(entity?.trailerPlate)"
/>
</QCard>
<QCard class="vn-one">
<VnLv :label="t('Phone')" :value="dashIfEmpty(entity?.phone)">
<template #value>
<span>
{{ dashIfEmpty(entity?.phone) }}
<VnLinkPhone :phone-number="entity?.phone" />
</span>
</template>
</VnLv>
<VnLv
:label="t('Worker')"
:value="
entity?.worker?.firstName
? `${entity?.worker?.firstName} ${entity?.worker?.lastName}`
: '-'
"
/>
<VnLv
:label="t('Observations')"
:value="dashIfEmpty(entity?.observations)"
/>
</QCard>
<QCard class="vn-max">
<div class="flex items-center">
<a class="header" :href="`#/route/roadmap/${entityId}/stops`">
{{ t('Stops') }}
<QIcon name="open_in_new" color="primary">
<QTooltip>
{{ t('Go to stops') }}
</QTooltip>
</QIcon>
</a>
<QIcon
name="add_circle"
color="primary"
class="q-ml-lg header cursor-pointer"
@click.stop="openAddStopDialog"
>
<QTooltip>
{{ t('Add stop') }}
</QTooltip>
</QIcon>
</div>
<QTable
:columns="columns"
:rows="entity?.expeditionTruck"
:rows-per-page-options="[0]"
row-key="id"
flat
hide-pagination
/>
</QCard>
</template>
</CardSummary>
</div>
</template>
<i18n>
es:
Carrier: Transportista
Tractor Plate: Matrícula tractor
Trailer Plate: Matrícula trailer
Phone: Teléfono
Worker: Trabajador
Observations: Observaciones
Stops: Paradas
Warehouse: Almacén
Go to stops: Ir a paradas
Add stop: Añadir parada
</i18n>

View File

@ -0,0 +1,344 @@
<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, toCurrency, toDate } from 'src/filters';
import VnSearchbar from 'components/ui/VnSearchbar.vue';
import RouteDescriptorProxy from 'pages/Route/Card/RouteDescriptorProxy.vue';
import InvoiceInDescriptorProxy from 'pages/InvoiceIn/Card/InvoiceInDescriptorProxy.vue';
import SupplierDescriptorProxy from 'pages/Supplier/Card/SupplierDescriptorProxy.vue';
import VnLv from 'components/ui/VnLv.vue';
import useNotify from 'composables/useNotify';
import RouteAutonomousFilter from 'pages/Route/Card/RouteAutonomousFilter.vue';
import { useRouter } from 'vue-router';
import RouteSummary from 'pages/Route/Card/RouteSummary.vue';
import { useSummaryDialog } from 'composables/useSummaryDialog';
import VnDms from 'components/common/VnDms.vue';
import { useState } from 'composables/useState';
import axios from 'axios';
const stateStore = useStateStore();
const { t } = useI18n();
const { notify } = useNotify();
const router = useRouter();
const { viewSummary } = useSummaryDialog();
onMounted(() => (stateStore.rightDrawer = true));
onUnmounted(() => (stateStore.rightDrawer = false));
const state = useState();
const user = state.getUser();
const dmsDialog = ref({
show: false,
initialForm: {},
rowsToCreateInvoiceIn: [],
});
const selectedRows = ref([]);
const columns = computed(() => [
{
name: 'ID',
label: t('ID'),
field: (row) => row.routeFk,
sortable: true,
align: 'left',
},
{
name: 'date',
label: t('Date'),
field: (row) => toDate(row.created),
sortable: true,
align: 'left',
},
{
name: 'agency-route',
label: t('Agency route'),
field: (row) => row?.agencyModeName,
sortable: true,
align: 'left',
},
{
name: 'agency-agreement',
label: t('Agency agreement'),
field: (row) => row?.agencyAgreement,
sortable: true,
align: 'left',
},
{
name: 'packages',
label: t('Packages'),
field: (row) => dashIfEmpty(row.packages),
sortable: true,
align: 'left',
},
{
name: 'volume',
label: 'm³',
field: (row) => dashIfEmpty(row.m3),
sortable: true,
align: 'left',
},
{
name: 'km',
label: t('Km'),
field: (row) => dashIfEmpty(row?.kmTotal),
sortable: true,
align: 'left',
},
{
name: 'price',
label: t('Price'),
field: (row) => (row?.price ? toCurrency(row.price) : '-'),
sortable: true,
align: 'left',
},
{
name: 'received',
label: t('Received'),
field: (row) => row?.invoiceInFk,
sortable: true,
align: 'left',
},
{
name: 'autonomous',
label: t('Autonomous'),
field: (row) => row?.supplierName,
sortable: true,
align: 'left',
},
{
name: 'actions',
label: '',
sortable: false,
align: 'right',
},
]);
const refreshKey = ref(0);
const total = computed(() => selectedRows.value.reduce((item) => item?.price || 0, 0));
const openDmsUploadDialog = async () => {
dmsDialog.value.rowsToCreateInvoiceIn = selectedRows.value
.filter(
(agencyTerm) => agencyTerm.supplierFk === selectedRows.value?.[0].supplierFk
)
.map((agencyTerm) => ({
routeFk: agencyTerm.routeFk,
supplierFk: agencyTerm.supplierFk,
created: agencyTerm.created,
totalPrice: total.value,
}));
if (dmsDialog.value.rowsToCreateInvoiceIn.length !== selectedRows.value.length) {
dmsDialog.value.rowsToCreateInvoiceIn = [];
dmsDialog.value.initialForm = null;
return notify(t('Two autonomous cannot be counted at the same time'), 'negative');
}
const dmsType = await axios
.get('DmsTypes/findOne', {
filter: {
where: { code: 'invoiceIn' },
},
})
.then((res) => res.data);
dmsDialog.value.initialForm = {
description: selectedRows.value?.[0].supplierName,
companyFk: user.value.companyFk,
warehouseFk: user.value.warehouseFk,
dmsTypeFk: dmsType?.id,
};
dmsDialog.value.show = true;
};
const onDmsSaved = async (dms, response) => {
if (response) {
await axios.post('AgencyTerms/createInvoiceIn', {
rows: dmsDialog.value.rowsToCreateInvoiceIn,
dms: response.data,
});
}
dmsDialog.value.show = false;
dmsDialog.value.initialForm = null;
dmsDialog.value.rowsToCreateInvoiceIn = [];
refreshKey.value++;
};
function navigateToRouteSummary(event, row) {
router.push({ name: 'RouteSummary', params: { id: row.routeFk } });
}
</script>
<template>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<VnSearchbar
data-key="RouteAutonomousList"
:label="t('Search autonomous')"
:info="t('You can search by autonomous reference')"
/>
</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>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<QScrollArea class="fit text-grey-8">
<RouteAutonomousFilter data-key="RouteAutonomousList" />
</QScrollArea>
</QDrawer>
<QPage class="column items-center">
<div class="route-list">
<div class="q-pa-md">
<QCard class="vn-one q-py-sm q-px-lg">
<VnLv class="flex">
<template #label>
<span class="text-h6">{{ t('Total') }}</span>
</template>
<template #value>
<span class="text-h6 q-ml-md">{{ toCurrency(total) }}</span>
</template>
</VnLv>
</QCard>
</div>
<VnPaginate
:key="refreshKey"
data-key="RouteAutonomousList"
url="AgencyTerms/filter"
:limit="20"
auto-load
>
<template #body="{ rows }">
<div class="q-pa-md">
<QTable
v-model:selected="selectedRows"
:columns="columns"
:rows="rows"
flat
row-key="routeFk"
selection="multiple"
:rows-per-page-options="[0]"
hide-pagination
@row-click="navigateToRouteSummary"
>
<template #body-cell-ID="props">
<QTd :props="props">
<span class="link" @click.stop>
{{ props.value }}
<RouteDescriptorProxy :id="props.value" />
</span>
</QTd>
</template>
<template #body-cell-received="props">
<QTd :props="props">
<span :class="props.value && 'link'" @click.stop>
{{ dashIfEmpty(props.value) }}
<InvoiceInDescriptorProxy
v-if="props.value"
:id="props.value"
/>
</span>
</QTd>
</template>
<template #body-cell-autonomous="props">
<QTd :props="props">
<span class="link" @click.stop>
{{ props.value }}
<SupplierDescriptorProxy
v-if="props.row?.supplierFk"
:id="props.row?.supplierFk"
/>
</span>
</QTd>
</template>
<template #body-cell-actions="props">
<QTd :props="props">
<div class="table-actions">
<QIcon
name="preview"
size="xs"
color="primary"
@click.stop="
viewSummary(
props?.row?.routeFk,
RouteSummary
)
"
>
<QTooltip>{{ t('Preview') }}</QTooltip>
</QIcon>
</div>
</QTd>
</template>
</QTable>
</div>
</template>
</VnPaginate>
</div>
<QPageSticky :offset="[20, 20]" v-if="selectedRows?.length">
<QBtn
fab
icon="vn:invoice-in-create"
color="primary"
@click="openDmsUploadDialog"
>
<QTooltip>
{{ t('Create invoiceIn') }}
</QTooltip>
</QBtn>
</QPageSticky>
</QPage>
<QDialog v-model="dmsDialog.show">
<VnDms
url="dms/uploadFile"
model="AgencyTerms"
:form-initial-data="dmsDialog.initialForm"
@on-data-saved="onDmsSaved"
/>
</QDialog>
</template>
<style lang="scss" scoped>
.route-list {
width: 100%;
}
.table-actions {
display: flex;
align-items: center;
gap: 12px;
i {
cursor: pointer;
}
}
</style>
<i18n>
es:
Search autonomous: Buscar autónomos
You can search by autonomous reference: Puedes buscar por referencia del autónomo
Create invoiceIn: Crear factura recibida
Two autonomous cannot be counted at the same time: Dos autonónomos no pueden ser contabilizados al mismo tiempo
Date: Fecha
Agency route: Agencia Ruta
Agency agreement: Agencia Acuerdo
Packages: Bultos
Price: Precio
Received: Recibida
Autonomous: Autónomos
Preview: Vista previa
</i18n>

View File

@ -2,9 +2,8 @@
import VnPaginate from 'components/ui/VnPaginate.vue'; import VnPaginate from 'components/ui/VnPaginate.vue';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import { computed, onMounted, onUnmounted, ref } from 'vue'; import { computed, onMounted, onUnmounted, ref } from 'vue';
import { dashIfEmpty, toDate, toHour } from 'src/filters'; import { dashIfEmpty, toHour } from 'src/filters';
import VnSelectFilter from 'components/common/VnSelectFilter.vue'; import VnSelectFilter from 'components/common/VnSelectFilter.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import { useValidator } from 'composables/useValidator'; import { useValidator } from 'composables/useValidator';
@ -16,12 +15,15 @@ import RouteSearchbar from 'pages/Route/Card/RouteSearchbar.vue';
import RouteFilter from 'pages/Route/Card/RouteFilter.vue'; import RouteFilter from 'pages/Route/Card/RouteFilter.vue';
import RouteSummary from 'pages/Route/Card/RouteSummary.vue'; import RouteSummary from 'pages/Route/Card/RouteSummary.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import {useSession} from "composables/useSession"; import { useSession } from 'composables/useSession';
import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import RouteListTicketsDialog from 'pages/Route/Card/RouteListTicketsDialog.vue';
import { useQuasar } from 'quasar';
const stateStore = useStateStore(); const stateStore = useStateStore();
const { t } = useI18n(); const { t } = useI18n();
const { validate } = useValidator(); const { validate } = useValidator();
const quasar = useQuasar();
const session = useSession(); const session = useSession();
const { viewSummary } = useSummaryDialog(); const { viewSummary } = useSummaryDialog();
@ -35,7 +37,7 @@ const columns = computed(() => [
label: t('ID'), label: t('ID'),
field: (row) => row.id, field: (row) => row.id,
sortable: true, sortable: true,
align: 'left', align: 'center',
}, },
{ {
name: 'worker', name: 'worker',
@ -70,7 +72,7 @@ const columns = computed(() => [
label: 'm³', label: 'm³',
field: (row) => dashIfEmpty(row.m3), field: (row) => dashIfEmpty(row.m3),
sortable: true, sortable: true,
align: 'left', align: 'center',
}, },
{ {
name: 'description', name: 'description',
@ -113,24 +115,6 @@ const updateRoute = async (route) => {
} }
}; };
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 confirmationDialog = ref(false);
const startingDate = ref(null); const startingDate = ref(null);
@ -144,21 +128,21 @@ const cloneRoutes = () => {
}; };
const showRouteReport = () => { const showRouteReport = () => {
const ids = selectedRows.value.map(row => row?.id) const ids = selectedRows.value.map((row) => row?.id);
const idString = ids.join(',') const idString = ids.join(',');
let url; let url;
if (selectedRows.value.length <= 1) { if (selectedRows.value.length <= 1) {
url = `api/Routes/${idString}/driver-route-pdf?access_token=${session.getToken()}`; url = `api/Routes/${idString}/driver-route-pdf?access_token=${session.getTokenMultimedia()}`;
} else { } else {
const params = new URLSearchParams({ const params = new URLSearchParams({
access_token: session.getToken(), access_token: session.getTokenMultimedia(),
id: idString id: idString,
}) });
url = `api/Routes/downloadZip?${params.toString()}`; url = `api/Routes/downloadZip?${params.toString()}`;
} }
window.open(url, '_blank'); window.open(url, '_blank');
} };
const markAsServed = () => { const markAsServed = () => {
selectedRows.value.forEach((row) => { selectedRows.value.forEach((row) => {
@ -169,6 +153,20 @@ const markAsServed = () => {
refreshKey.value++; refreshKey.value++;
startingDate.value = null; startingDate.value = null;
}; };
const openTicketsDialog = (id) => {
if (!id) {
return;
}
quasar
.dialog({
component: RouteListTicketsDialog,
componentProps: {
id,
},
})
.onOk(() => refreshKey.value++);
};
</script> </script>
<template> <template>
@ -287,31 +285,22 @@ const markAsServed = () => {
:rows-per-page-options="[0]" :rows-per-page-options="[0]"
hide-pagination hide-pagination
:pagination="{ sortBy: 'ID', descending: true }" :pagination="{ sortBy: 'ID', descending: true }"
:no-data-label="t('globals.noResults')"
> >
<template #body-cell-worker="props"> <template #body-cell-worker="{ row }">
<QTd :props="props"> <QTd>
{{ props.row?.workerUserName }}
<QPopupEdit
:model-value="props.row.workerFk"
v-slot="scope"
buttons
@update:model-value="
(worker) => updateWorker(props.row, worker)
"
>
<VnSelectFilter <VnSelectFilter
:label="t('Worker')" :label="t('Worker')"
v-model="scope.value" v-model="row.workerFk"
:options="workers" :options="workers"
option-value="id" option-value="id"
option-label="name" option-label="nickname"
hide-selected hide-selected
autofocus dense
:emit-value="false" :emit-value="false"
:rules="validate('Route.workerFk')" :rules="validate('Route.workerFk')"
:is-clearable="false" :is-clearable="false"
@keyup.enter="scope.set" @update:model-value="updateRoute(row)"
@focus="($event) => $event.target.select()"
> >
<template #option="{ opt, itemProps }"> <template #option="{ opt, itemProps }">
<QItem <QItem
@ -329,157 +318,103 @@ const markAsServed = () => {
</QItem> </QItem>
</template> </template>
</VnSelectFilter> </VnSelectFilter>
</QPopupEdit>
</QTd> </QTd>
</template> </template>
<template #body-cell-agency="props"> <template #body-cell-agency="{ row }">
<QTd :props="props"> <QTd>
{{ props.row?.agencyName }}
<QPopupEdit
:model-value="props.row.agencyModeFk"
v-slot="scope"
buttons
@update:model-value="
(agency) => updateAgency(props.row, agency)
"
>
<VnSelectFilter <VnSelectFilter
:label="t('Agency')" :label="t('Agency')"
v-model="scope.value" v-model="row.agencyModeFk"
:options="agencyList" :options="agencyList"
option-value="id" option-value="id"
option-label="name" option-label="name"
hide-selected hide-selected
autofocus dense
:emit-value="false" :emit-value="false"
:rules="validate('route.agencyFk')" :rules="validate('route.agencyFk')"
:is-clearable="false" :is-clearable="false"
@keyup.enter="scope.set" @update:model-value="updateRoute(row)"
@focus="($event) => $event.target.select()"
/> />
</QPopupEdit>
</QTd> </QTd>
</template> </template>
<template #body-cell-vehicle="props"> <template #body-cell-vehicle="{ row }">
<QTd :props="props"> <QTd>
{{ props.row?.vehiclePlateNumber }}
<QPopupEdit
:model-value="props.row.vehicleFk"
v-slot="scope"
buttons
@update:model-value="
(vehicle) => updateVehicle(props.row, vehicle)
"
>
<VnSelectFilter <VnSelectFilter
:label="t('Vehicle')" :label="t('Vehicle')"
v-model="scope.value" v-model="row.vehicleFk"
:options="vehicleList" :options="vehicleList"
option-value="id" option-value="id"
option-label="numberPlate" option-label="numberPlate"
hide-selected hide-selected
autofocus dense
:emit-value="false" :emit-value="false"
:rules="validate('route.vehicleFk')" :rules="validate('route.vehicleFk')"
:is-clearable="false" :is-clearable="false"
@keyup.enter="scope.set" @update:model-value="updateRoute(row)"
@focus="($event) => $event.target.select()"
/> />
</QPopupEdit>
</QTd> </QTd>
</template> </template>
<template #body-cell-date="props"> <template #body-cell-date="{ row }">
<QTd :props="props"> <QTd class="table-input-cell">
{{ toDate(props.row?.created) }}
<QPopupEdit
v-model="props.row.created"
v-slot="scope"
@update:model-value="updateRoute(props.row)"
buttons
>
<VnInputDate <VnInputDate
v-model="scope.value" v-model="row.created"
autofocus hide-bottom-space
dense
:label="t('Date')" :label="t('Date')"
:rules="validate('route.created')" :rules="validate('route.created')"
:is-clearable="false" :is-clearable="false"
@keyup.enter="scope.set" @update:model-value="updateRoute(row)"
@focus="($event) => $event.target.select()"
/> />
</QPopupEdit>
</QTd> </QTd>
</template> </template>
<template #body-cell-description="props"> <template #body-cell-description="{ row }">
<QTd :props="props"> <QTd class="table-input-cell">
{{ props.row?.description }}
<QPopupEdit
v-model="props.row.description"
v-slot="scope"
@update:model-value="updateRoute(props.row)"
buttons
>
<VnInput <VnInput
v-model="scope.value" v-model="row.description"
autofocus
:label="t('Description')" :label="t('Description')"
:rules="validate('route.description')" :rules="validate('route.description')"
:is-clearable="false" :is-clearable="false"
@keyup.enter="scope.set" dense
@focus="($event) => $event.target.select()" @update:model-value="updateRoute(row)"
/> />
</QPopupEdit>
</QTd> </QTd>
</template> </template>
<template #body-cell-started="props"> <template #body-cell-started="{ row }">
<QTd :props="props"> <QTd class="table-input-cell">
{{ toHour(props.row.started) }}
<QPopupEdit
v-model="props.row.started"
v-slot="scope"
buttons
@update:model-value="updateRoute(props.row)"
>
<VnInputTime <VnInputTime
v-model="scope.value" v-model="row.started"
autofocus
:label="t('Hour started')" :label="t('Hour started')"
:rules="validate('route.started')" :rules="validate('route.started')"
:is-clearable="false" :is-clearable="false"
@keyup.enter="scope.set" hide-bottom-space
@focus="($event) => $event.target.select()" dense
@update:model-value="updateRoute(row)"
/> />
</QPopupEdit>
</QTd> </QTd>
</template> </template>
<template #body-cell-finished="props"> <template #body-cell-finished="{ row }">
<QTd :props="props"> <QTd class="table-input-cell">
{{ toHour(props.row.finished) }}
<QPopupEdit
v-model="props.row.finished"
v-slot="scope"
buttons
@update:model-value="updateRoute(props.row)"
>
<VnInputTime <VnInputTime
v-model="scope.value" v-model="row.finished"
autofocus autofocus
:label="t('Hour finished')" :label="t('Hour finished')"
:rules="validate('route.finished')" :rules="validate('route.finished')"
:is-clearable="false" :is-clearable="false"
@keyup.enter="scope.set" hide-bottom-space
@focus="($event) => $event.target.select()" dense
@update:model-value="updateRoute(row)"
/> />
</QPopupEdit>
</QTd> </QTd>
</template> </template>
<template #body-cell-actions="props"> <template #body-cell-actions="props">
<QTd :props="props"> <QTd :props="props">
<div class="flex items-center table-actions"> <div class="flex items-center no-wrap table-actions">
<QIcon <QIcon
name="vn:ticketAdd" name="vn:ticketAdd"
size="xs" size="xs"
color="primary" color="primary"
class="cursor-pointer" class="cursor-pointer"
@click="openTicketsDialog(props?.row?.id)"
> >
<QTooltip>{{ t('Add ticket') }}</QTooltip> <QTooltip>{{ t('Add ticket') }}</QTooltip>
</QIcon> </QIcon>
@ -494,6 +429,20 @@ const markAsServed = () => {
> >
<QTooltip>{{ t('Preview') }}</QTooltip> <QTooltip>{{ t('Preview') }}</QTooltip>
</QIcon> </QIcon>
<RouterLink
:to="{
name: 'RouteSummary',
params: { id: props?.row?.id },
}"
>
<QIcon
name="vn:eye"
size="xs"
color="primary"
>
<QTooltip>{{ t('Summary') }}</QTooltip>
</QIcon>
</RouterLink>
</div> </div>
</QTd> </QTd>
</template> </template>
@ -508,8 +457,13 @@ const markAsServed = () => {
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.table-input-cell {
min-width: 150px;
}
.route-list { .route-list {
width: 100%; width: 100%;
max-height: 100%;
} }
.table-actions { .table-actions {
@ -538,4 +492,5 @@ es:
Download selected routes as PDF: Descargar rutas seleccionadas como PDF Download selected routes as PDF: Descargar rutas seleccionadas como PDF
Add ticket: Añadir tickets Add ticket: Añadir tickets
Preview: Vista previa Preview: Vista previa
Summary: Resumen
</i18n> </i18n>

View File

@ -0,0 +1,6 @@
<script setup>
import VnLog from 'src/components/common/VnLog.vue';
</script>
<template>
<VnLog model="Route" url="/RouteLogs"></VnLog>
</template>

View File

@ -0,0 +1,296 @@
<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, toDateHour } from 'src/filters';
import { useQuasar } from 'quasar';
import toCurrency from 'filters/toCurrency';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import SupplierDescriptorProxy from 'pages/Supplier/Card/SupplierDescriptorProxy.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue';
import RoadmapFilter from 'pages/Route/Roadmap/RoadmapFilter.vue';
import VnConfirm from 'components/ui/VnConfirm.vue';
import axios from 'axios';
import VnInputDate from 'components/common/VnInputDate.vue';
import {useSummaryDialog} from "composables/useSummaryDialog";
import RoadmapSummary from "pages/Route/Roadmap/RoadmapSummary.vue";
import {useRouter} from "vue-router";
const stateStore = useStateStore();
const { t } = useI18n();
const quasar = useQuasar();
const router = useRouter();
const { viewSummary } = useSummaryDialog();
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);
onMounted(() => (stateStore.rightDrawer = true));
onUnmounted(() => (stateStore.rightDrawer = false));
const selectedRows = ref([]);
const columns = computed(() => [
{
name: 'roadmap',
label: t('Roadmap'),
field: (row) => row.name,
sortable: true,
align: 'left',
},
{
name: 'ETD',
label: t('ETD'),
field: (row) => toDateHour(row.etd),
sortable: true,
align: 'left',
},
{
name: 'carrier',
label: t('Carrier'),
field: (row) => row.supplier?.nickname,
sortable: true,
align: 'left',
},
{
name: 'plate',
label: t('Plate'),
field: (row) => row.tractorPlate,
sortable: true,
align: 'left',
},
{
name: 'price',
label: t('Price'),
field: (row) => toCurrency(row.price),
sortable: true,
align: 'left',
},
{
name: 'observations',
label: t('Observations'),
field: (row) => dashIfEmpty(row.observations),
sortable: true,
align: 'left',
},
{
name: 'actions',
label: '',
sortable: false,
align: 'right',
},
]);
const refreshKey = ref(0);
const isCloneDialogOpen = ref(false);
const etdDate = ref(null);
const filter = {
include: { relation: 'supplier', scope: { fields: ['nickname'] } },
where: {
and: [{ etd: { gte: from } }, { etd: { lte: to } }],
},
};
const cloneSelection = async () => {
await axios.post('Roadmaps/clone', {
etd: etdDate.value,
ids: selectedRows.value.map((row) => row?.id),
});
refreshKey.value++;
etdDate.value = null;
};
const removeSelection = async () => {
await Promise.all(
selectedRows.value.map((roadmap) => {
axios.delete(`Roadmaps/${roadmap.id}`);
})
);
};
function confirmRemove() {
quasar
.dialog({
component: VnConfirm,
componentProps: {
title: t('Selected roadmaps will be removed'),
message: t('Are you sure you want to continue?'),
promise: removeSelection,
},
})
.onOk(() => refreshKey.value++);
}
function navigateToRoadmapSummary(event, row) {
router.push({ name: 'RoadmapSummary', params: { id: row.id } })
}
</script>
<template>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<VnSearchbar
data-key="RoadmapList"
:label="t('Search roadmaps')"
:info="t('You can search by roadmap reference')"
/>
</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>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<QScrollArea class="fit text-grey-8">
<RoadmapFilter data-key="RoadmapList" />
</QScrollArea>
</QDrawer>
<QDialog v-model="isCloneDialogOpen">
<QCard style="min-width: 350px">
<QCardSection>
<p class="text-h6 q-ma-none">
{{ t('Select the estimated date of departure (ETD)') }}
</p>
</QCardSection>
<QCardSection class="q-pt-none">
<VnInputDate :label="t('ETD')" v-model="etdDate" autofocus />
</QCardSection>
<QCardActions align="right">
<QBtn flat :label="t('Cancel')" v-close-popup class="text-primary" />
<QBtn color="primary" v-close-popup @click="cloneSelection">
{{ t('Clone') }}
</QBtn>
</QCardActions>
</QCard>
</QDialog>
<QPage class="column items-center">
<VnSubToolbar class="bg-vn-dark justify-end">
<template #st-actions>
<QBtn
icon="vn:clone"
color="primary"
class="q-mr-sm"
:disable="!selectedRows?.length"
@click="isCloneDialogOpen = true"
>
<QTooltip>{{ t('Clone Selected Routes') }}</QTooltip>
</QBtn>
<QBtn
icon="delete"
color="primary"
class="q-mr-sm"
:disable="!selectedRows?.length"
@click="confirmRemove"
>
<QTooltip>{{ t('Delete roadmap(s)') }}</QTooltip>
</QBtn>
</template>
</VnSubToolbar>
<div class="route-list">
<VnPaginate
:key="refreshKey"
data-key="RoadmapList"
url="Roadmaps"
:limit="20"
:filter="filter"
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
:pagination="{ sortBy: 'ID', descending: true }"
@row-click="navigateToRoadmapSummary"
>
<template #body-cell-carrier="props">
<QTd :props="props">
<span v-if="props.value" class="link" @click.stop>
{{ props.value }}
<SupplierDescriptorProxy
:id="props.row?.supplier?.id"
/>
</span>
</QTd>
</template>
<template #body-cell-actions="props">
<QTd :props="props">
<div class="flex items-center table-actions">
<QIcon
name="preview"
size="xs"
color="primary"
@click.stop="viewSummary(props?.row?.id, RoadmapSummary)"
class="cursor-pointer"
>
<QTooltip>{{ t('Preview') }}</QTooltip>
</QIcon>
</div>
</QTd>
</template>
</QTable>
</div>
</template>
</VnPaginate>
</div>
<QPageSticky :offset="[20, 20]">
<RouterLink :to="{ name: 'RouteRoadmapCreate' }">
<QBtn fab icon="add" color="primary" />
<QTooltip>
{{ t('Create roadmap') }}
</QTooltip>
</RouterLink>
</QPageSticky>
</QPage>
</template>
<style lang="scss" scoped>
.route-list {
width: 100%;
}
.table-actions {
gap: 12px;
}
</style>
<i18n>
es:
Search roadmaps: Buscar troncales
You can search by roadmap reference: Puedes buscar por referencia de la troncal
Delete roadmap(s): Eliminar troncal(es)
Selected roadmaps will be removed: Las troncales seleccionadas serán eliminadas
Are you sure you want to continue?: ¿Seguro que quieres continuar?
The date can't be empty: La fecha no puede estar vacía
Clone Selected Routes: Clonar rutas seleccionadas
Create roadmap: Crear troncal
Roadmap: Troncal
Carrier: Transportista
Plate: Matrícula
Price: Precio
Observations: Observaciones
Preview: Vista previa
</i18n>

View File

@ -0,0 +1,453 @@
<script setup>
import VnPaginate from 'components/ui/VnPaginate.vue';
import { useI18n } from 'vue-i18n';
import { computed, ref } from 'vue';
import { dashIfEmpty } from 'src/filters';
import VnInputDate from 'components/common/VnInputDate.vue';
import VnInput from 'components/common/VnInput.vue';
import axios from 'axios';
import { QIcon, useQuasar } from 'quasar';
import RouteListTicketsDialog from 'pages/Route/Card/RouteListTicketsDialog.vue';
import TicketDescriptorProxy from 'pages/Ticket/Card/TicketDescriptorProxy.vue';
import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy.vue';
import { useRoute } from 'vue-router';
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';
import RouteSearchbar from "pages/Route/Card/RouteSearchbar.vue";
import {useStateStore} from "stores/useStateStore";
const { t } = useI18n();
const quasar = useQuasar();
const route = useRoute();
const stateStore = useStateStore();
const selectedRows = ref([]);
const columns = computed(() => [
{
name: 'order',
label: t('Order'),
field: (row) => dashIfEmpty(row?.priority),
sortable: false,
align: 'center',
},
{
name: 'street',
label: t('Street'),
field: (row) => row?.street,
sortable: false,
align: 'left',
},
{
name: 'city',
label: t('City'),
field: (row) => row?.city,
sortable: false,
align: 'left',
},
{
name: 'pc',
label: t('PC'),
field: (row) => row?.postalCode,
sortable: false,
align: 'center',
},
{
name: 'client',
label: t('Client'),
field: (row) => row?.nickname,
sortable: false,
align: 'left',
},
{
name: 'warehouse',
label: t('Warehouse'),
field: (row) => row?.warehouseName,
sortable: false,
align: 'left',
},
{
name: 'packages',
label: t('Packages'),
field: (row) => row?.packages,
sortable: false,
align: 'center',
},
{
name: 'volume',
label: 'm³',
field: (row) => row?.volume,
sortable: false,
align: 'center',
},
{
name: 'packaging',
label: t('Packaging'),
field: (row) => row?.ipt,
sortable: false,
align: 'center',
},
{
name: 'ticket',
label: t('Ticket'),
field: (row) => row?.id,
sortable: false,
align: 'center',
},
{
name: 'observations',
label: '',
field: (row) => row?.ticketObservation,
sortable: false,
align: 'left',
},
]);
const refreshKey = ref(0);
const confirmationDialog = ref(false);
const startingDate = ref(null);
const routeEntity = ref(null);
const ticketList = ref([]);
const cloneRoutes = () => {
axios.post('Routes/clone', {
created: startingDate.value,
ids: selectedRows.value.map((row) => row?.id),
});
refreshKey.value++;
startingDate.value = null;
};
const deletePriorities = async () => {
try {
await Promise.all(
selectedRows.value.map((ticket) =>
axios.patch(`Tickets/${ticket?.id}/`, { priority: null })
)
);
} finally {
refreshKey.value++;
}
};
const setOrderedPriority = async () => {
try {
await Promise.all(
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++;
};
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 = () => {
quasar
.dialog({
component: RouteListTicketsDialog,
componentProps: {
id: route.params.id,
},
})
.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('Confirm removal from route'),
message: t('Are you sure you want to remove this ticket from the route?'),
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>
<template>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<RouteSearchbar />
</Teleport>
</template>
<FetchData
@on-fetch="(data) => (routeEntity = data)"
auto-load
:url="`Routes/${route.params?.id}`"
:filter="{
fields: ['vehicleFk'],
}"
/>
<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>
<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>
<QPage class="column items-center">
<QToolbar class="bg-vn-dark justify-end">
<div id="st-actions" class="q-pa-sm">
<QBtn icon="vn:wand" color="primary" class="q-mr-sm" @click="sortRoutes">
<QTooltip>{{ t('Sort routes') }}</QTooltip>
</QBtn>
<QBtn
icon="vn:buscaman"
color="primary"
class="q-mr-sm"
:disable="!selectedRows?.length"
@click="goToBuscaman()"
>
<QTooltip>{{ t('Open buscaman') }}</QTooltip>
</QBtn>
<QBtn
icon="filter_alt"
color="primary"
class="q-mr-sm filled-icon"
:disable="!selectedRows?.length"
@click="deletePriorities"
>
<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"
color="primary"
class="q-mr-sm filled-icon"
:disable="!selectedRows?.length"
@click="openSmsDialog"
>
<QTooltip>{{ t('Send SMS to all clients') }}</QTooltip>
</QBtn>
</div>
</QToolbar>
<div class="route-list">
<VnPaginate
:key="refreshKey"
data-key="RouteTicketList"
url="Routes/getTickets"
:filter="{ id: route.params.id }"
:order="['priority ASC']"
auto-load
@on-fetch="(data) => (ticketList = data)"
>
<template #body="{ rows }">
<div class="q-pa-md">
<QTable
v-model:selected="selectedRows"
:columns="columns"
:rows="rows"
:rows-per-page-options="[0]"
row-key="id"
flat
hide-pagination
selection="multiple"
>
<template #body-cell-order="{ row }">
<QTd class="order-field">
<div class="flex no-wrap items-center">
<QIcon
name="low_priority"
size="xs"
class="q-mr-md cursor-pointer"
color="primary"
@click="setHighestPriority(row, rows)"
>
<QTooltip>
{{ t('Assign highest priority') }}
</QTooltip>
</QIcon>
<VnInput
v-model="row.priority"
dense
@blur="updatePriority(row)"
/>
</div>
</QTd>
</template>
<template #body-cell-city="{ value, row }">
<QTd auto-width>
<span
class="text-primary cursor-pointer"
@click="goToBuscaman(row)"
>
{{ value }}
<QTooltip>{{ t('Open buscaman') }}</QTooltip>
</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 class="text-center">
<span class="text-primary cursor-pointer">
{{ value }}
<TicketDescriptorProxy :id="row?.id" />
</span>
</QTd>
</template>
<template #body-cell-observations="{ value, row }">
<QTd auto-width>
<div class="flex items-center no-wrap table-actions">
<QIcon
name="delete"
color="primary"
class="cursor-pointer"
size="xs"
@click="confirmRemove(row)"
>
<QTooltip>{{ t('globals.remove') }}</QTooltip>
</QIcon>
<QIcon
v-if="value"
name="vn:notes"
color="primary"
class="cursor-pointer"
>
<QTooltip>{{ value }}</QTooltip>
</QIcon>
</div>
</QTd>
</template>
</QTable>
</div>
</template>
</VnPaginate>
</div>
<QPageSticky :offset="[20, 20]">
<QBtn fab icon="add" color="primary" @click="openTicketsDialog">
<QTooltip>
{{ t('Add ticket') }}
</QTooltip>
</QBtn>
</QPageSticky>
</QPage>
</template>
<style lang="scss" scoped>
.route-list {
width: 100%;
}
.order-field {
max-width: 140px;
min-width: 120px;
}
.table-actions {
gap: 12px;
}
.filled-icon {
font-variation-settings: 'FILL' 1;
}
</style>
<i18n>
es:
Order: Orden
Street: Dirección fiscal
City: Población
PC: CP
Client: Cliente
Warehouse: Almacén
Packages: Bultos
Packaging: Encajado
Confirm removal from route: Quitar de la ruta
Are you sure you want to remove this ticket from the route?: ¿Seguro que quieres quitar este ticket de la ruta?
Sort routes: Ordenar rutas
Open buscaman: Abrir buscaman
Delete priority: Borrar orden
Renumber all tickets in the order you see on the screen: Renumerar todos los tickets con el orden que ves por pantalla
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
Add ticket: Añadir ticket
</i18n>

View File

@ -18,13 +18,16 @@ const quasar = useQuasar();
const { notify } = useNotify(); const { notify } = useNotify();
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const bankEntitiesRef = ref(null);
const supplier = ref(null); const supplier = ref(null);
const supplierAccountRef = ref(null); const supplierAccountRef = ref(null);
const wireTransferFk = ref(null); const wireTransferFk = ref(null);
const bankEntitiesOptions = ref([]); const bankEntitiesOptions = ref([]);
const onBankEntityCreated = (data) => { const onBankEntityCreated = async (dataSaved, rowData) => {
bankEntitiesOptions.value.push(data); await bankEntitiesRef.value.fetch();
rowData.bankEntityFk = dataSaved.id;
}; };
const onChangesSaved = () => { const onChangesSaved = () => {
@ -63,6 +66,7 @@ onMounted(() => {
</script> </script>
<template> <template>
<FetchData <FetchData
ref="bankEntitiesRef"
url="BankEntities" url="BankEntities"
@on-fetch="(data) => (bankEntitiesOptions = data)" @on-fetch="(data) => (bankEntitiesOptions = data)"
auto-load auto-load
@ -114,13 +118,16 @@ onMounted(() => {
:label="t('worker.create.bankEntity')" :label="t('worker.create.bankEntity')"
v-model="row.bankEntityFk" v-model="row.bankEntityFk"
:options="bankEntitiesOptions" :options="bankEntitiesOptions"
option-label="name" option-label="bic"
option-value="id" option-value="id"
hide-selected hide-selected
> >
<template #form> <template #form>
<CreateBankEntityForm <CreateBankEntityForm
@on-data-saved="onBankEntityCreated($event)" @on-data-saved="
(_, requestResponse) =>
onBankEntityCreated(requestResponse, row)
"
:show-entity-field="false" :show-entity-field="false"
/> />
</template> </template>

View File

@ -83,8 +83,13 @@ const redirectToUpdateView = (addressData) => {
<QPageSticky :offset="[20, 20]"> <QPageSticky :offset="[20, 20]">
<QBtn fab icon="add" color="primary" @click="redirectToCreateView()" /> <QBtn fab icon="add" color="primary" @click="redirectToCreateView()" />
<QTooltip> <QTooltip>
{{ t('supplier.list.newSupplier') }} {{ t('New address') }}
</QTooltip> </QTooltip>
</QPageSticky> </QPageSticky>
</QPage> </QPage>
</template> </template>
<i18n>
es:
New address: Nueva dirección
</i18n>

View File

@ -26,6 +26,7 @@ const workersOptions = ref([]);
:url-update="`Suppliers/${route.params.id}`" :url-update="`Suppliers/${route.params.id}`"
model="supplier" model="supplier"
auto-load auto-load
:clear-store-on-unmount="false"
> >
<template #form="{ data, validate }"> <template #form="{ data, validate }">
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">

View File

@ -33,6 +33,7 @@ const formatPayDems = (data) => {
:url-update="`Suppliers/${route.params.id}`" :url-update="`Suppliers/${route.params.id}`"
model="supplier" model="supplier"
auto-load auto-load
:clear-store-on-unmount="false"
> >
<template #form="{ data, validate }"> <template #form="{ data, validate }">
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue'; import { ref, onMounted, nextTick } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@ -11,6 +11,15 @@ const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const supplierContactRef = ref(null); const supplierContactRef = ref(null);
const insertRow = () => {
supplierContactRef.value.insert();
nextTick(() => {
const inputs = document.querySelectorAll('[input-name-focusable]');
const lastInput = inputs[inputs.length - 1];
if (lastInput) lastInput.focus();
});
};
onMounted(() => { onMounted(() => {
if (supplierContactRef.value) supplierContactRef.value.reload(); if (supplierContactRef.value) supplierContactRef.value.reload();
}); });
@ -38,6 +47,7 @@ onMounted(() => {
<VnRow class="row q-gutter-md"> <VnRow class="row q-gutter-md">
<div class="col"> <div class="col">
<VnInput <VnInput
input-name-focusable
:label="t('supplier.contacts.name')" :label="t('supplier.contacts.name')"
v-model="row.name" v-model="row.name"
/> />
@ -92,7 +102,7 @@ onMounted(() => {
size="sm" size="sm"
class="cursor-pointer" class="cursor-pointer"
color="primary" color="primary"
@click="supplierContactRef.insert()" @click="insertRow()"
> >
<QTooltip> <QTooltip>
{{ t('Add contact') }} {{ t('Add contact') }}

View File

@ -9,6 +9,7 @@ import VnLv from 'src/components/ui/VnLv.vue';
import { toDateString } from 'src/filters'; import { toDateString } from 'src/filters';
import useCardDescription from 'src/composables/useCardDescription'; import useCardDescription from 'src/composables/useCardDescription';
import { getUrl } from 'src/composables/getUrl'; import { getUrl } from 'src/composables/getUrl';
import { useState } from 'src/composables/useState';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -21,6 +22,7 @@ const $props = defineProps({
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const url = ref(); const url = ref();
const state = useState();
const filter = { const filter = {
fields: [ fields: [
@ -71,6 +73,8 @@ const setData = (entity) => {
data.value = useCardDescription(entity.ref, entity.id); data.value = useCardDescription(entity.ref, entity.id);
}; };
const supplier = computed(() => state.get('supplier'));
const getEntryQueryParams = (supplier) => { const getEntryQueryParams = (supplier) => {
if (!supplier) return null; if (!supplier) return null;
@ -101,7 +105,7 @@ const getEntryQueryParams = (supplier) => {
:subtitle="data.subtitle" :subtitle="data.subtitle"
:filter="filter" :filter="filter"
@on-fetch="setData" @on-fetch="setData"
data-key="Supplier" data-key="supplier"
> >
<template #header-extra-action> <template #header-extra-action>
<QBtn <QBtn
@ -133,10 +137,10 @@ const getEntryQueryParams = (supplier) => {
<VnLv :label="t('supplier.summary.payDay')" :value="entity.payDay" /> <VnLv :label="t('supplier.summary.payDay')" :value="entity.payDay" />
<VnLv :label="t('supplier.summary.account')" :value="entity.account" /> <VnLv :label="t('supplier.summary.account')" :value="entity.account" />
</template> </template>
<template #icons="{ entity }"> <template #icons>
<QCardActions class="q-gutter-x-md"> <QCardActions v-if="supplier" class="q-gutter-x-md">
<QIcon <QIcon
v-if="!entity.isActive" v-if="!supplier.isActive"
name="vn:disabled" name="vn:disabled"
color="primary" color="primary"
size="xs" size="xs"
@ -144,7 +148,7 @@ const getEntryQueryParams = (supplier) => {
<QTooltip>{{ t('Inactive supplier') }}</QTooltip> <QTooltip>{{ t('Inactive supplier') }}</QTooltip>
</QIcon> </QIcon>
<QIcon <QIcon
v-if="!entity.isSerious" v-if="!supplier.isSerious"
name="vn:supplierfalse" name="vn:supplierfalse"
color="primary" color="primary"
size="xs" size="xs"
@ -167,6 +171,7 @@ const getEntryQueryParams = (supplier) => {
<QTooltip>{{ t('All entries with current supplier') }}</QTooltip> <QTooltip>{{ t('All entries with current supplier') }}</QTooltip>
</QBtn> </QBtn>
<QBtn <QBtn
v-if="entity.client?.fi"
:to="{ :to="{
name: 'CustomerCard', name: 'CustomerCard',
params: { id: entity.client?.id }, params: { id: entity.client?.id },

View File

@ -53,6 +53,7 @@ function handleLocation(data, location) {
:url-update="`Suppliers/${route.params.id}/updateFiscalData`" :url-update="`Suppliers/${route.params.id}/updateFiscalData`"
model="supplier" model="supplier"
auto-load auto-load
:clear-store-on-unmount="false"
> >
<template #form="{ data, validate }"> <template #form="{ data, validate }">
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">

View File

@ -51,25 +51,20 @@ const isAdministrative = computed(() => {
:url="`Suppliers/${entityId}/getSummary`" :url="`Suppliers/${entityId}/getSummary`"
@on-fetch="(data) => setData(data)" @on-fetch="(data) => setData(data)"
> >
<template #header-left>
<a v-if="isAdministrative" class="header header-link" :href="supplierUrl">
<QIcon name="open_in_new" color="white" size="sm" />
</a>
</template>
<template #header> <template #header>
<span>{{ supplier.name }} - {{ supplier.id }}</span> <span>{{ supplier.name }} - {{ supplier.id }}</span>
</template> </template>
<template #body> <template #body>
<QCard class="vn-one"> <QCard class="vn-one">
<a <router-link
v-if="isAdministrative" v-if="isAdministrative"
class="header header-link" class="header link"
:href="`#/supplier/${entityId}/basic-data`" :to="{ name: 'SupplierBasicData', params: { id: entityId } }"
> >
{{ t('globals.summary.basicData') }} {{ t('globals.summary.basicData') }}
<QIcon name="open_in_new" /> <QIcon name="open_in_new" />
</a> </router-link>
<span v-else> {{ t('globals.summary.basicData') }}</span> <span v-else> {{ t('globals.summary.basicData') }}</span>
<VnLv label="Id" :value="supplier.id" /> <VnLv label="Id" :value="supplier.id" />
<VnLv label="Alias" :value="supplier.nickname" /> <VnLv label="Alias" :value="supplier.nickname" />
@ -98,14 +93,14 @@ const isAdministrative = computed(() => {
/> />
</QCard> </QCard>
<QCard class="vn-one"> <QCard class="vn-one">
<a <router-link
v-if="isAdministrative" v-if="isAdministrative"
class="header header-link" class="header link"
:href="`#/supplier/${entityId}/billing-data`" :to="{ name: 'SupplierBillingData', params: { id: entityId } }"
> >
{{ t('supplier.summary.billingData') }} {{ t('supplier.summary.billingData') }}
<QIcon name="open_in_new" /> <QIcon name="open_in_new" />
</a> </router-link>
<span v-else> {{ t('supplier.summary.billingData') }}</span> <span v-else> {{ t('supplier.summary.billingData') }}</span>
<VnLv <VnLv
:label="t('supplier.summary.payMethod')" :label="t('supplier.summary.payMethod')"
@ -121,14 +116,14 @@ const isAdministrative = computed(() => {
<VnLv :label="t('supplier.summary.account')" :value="supplier.account" /> <VnLv :label="t('supplier.summary.account')" :value="supplier.account" />
</QCard> </QCard>
<QCard class="vn-one"> <QCard class="vn-one">
<a <router-link
v-if="isAdministrative" v-if="isAdministrative"
class="header header-link" class="header link"
:href="`#/supplier/${entityId}/fiscal-data`" :to="{ name: 'SupplierFiscalData', params: { id: entityId } }"
> >
{{ t('supplier.summary.fiscalData') }} {{ t('supplier.summary.fiscalData') }}
<QIcon name="open_in_new" /> <QIcon name="open_in_new" />
</a> </router-link>
<span v-else> {{ t('supplier.summary.fiscalData') }}</span> <span v-else> {{ t('supplier.summary.fiscalData') }}</span>
<VnLv <VnLv
:label="t('supplier.summary.sageTaxType')" :label="t('supplier.summary.sageTaxType')"
@ -156,14 +151,14 @@ const isAdministrative = computed(() => {
/> />
</QCard> </QCard>
<QCard class="vn-one"> <QCard class="vn-one">
<a <router-link
v-if="isAdministrative" v-if="isAdministrative"
class="header header-link" class="header link"
:href="`#/supplier/${entityId}/fiscal-data`" :to="{ name: 'SupplierFiscalData', params: { id: entityId } }"
> >
{{ t('supplier.summary.fiscalAddress') }} {{ t('supplier.summary.fiscalAddress') }}
<QIcon name="open_in_new" /> <QIcon name="open_in_new" />
</a> </router-link>
<span v-else> {{ t('supplier.summary.fiscalAddress') }}</span> <span v-else> {{ t('supplier.summary.fiscalAddress') }}</span>
<VnLv :label="t('supplier.summary.socialName')" :value="supplier.name" /> <VnLv :label="t('supplier.summary.socialName')" :value="supplier.name" />
<VnLv :label="t('supplier.summary.taxNumber')" :value="supplier.nif" /> <VnLv :label="t('supplier.summary.taxNumber')" :value="supplier.nif" />

View File

@ -42,7 +42,7 @@ const redirectToCreateView = () => {
</QScrollArea> </QScrollArea>
</QDrawer> </QDrawer>
<div class="vn-card-list"> <div class="vn-card-list">
<VnPaginate data-key="SuppliersList" url="Suppliers/filter" auto-load> <VnPaginate data-key="SuppliersList" url="Suppliers/filter">
<template #body="{ rows }"> <template #body="{ rows }">
<CardList <CardList
v-for="row of rows" v-for="row of rows"

View File

@ -236,19 +236,9 @@ async function setTravelData(travelData) {
:url="`Travels/${entityId}/getTravel`" :url="`Travels/${entityId}/getTravel`"
@on-fetch="(data) => setTravelData(data)" @on-fetch="(data) => setTravelData(data)"
> >
<template #header-left>
<router-link
class="header link"
:to="{ name: 'TravelSummary', params: { id: entityId } }"
>
<QIcon name="open_in_new" color="white" size="sm" />
<QTooltip>{{ t('travel.pageTitles.summary') }}</QTooltip>
</router-link>
</template>
<template #header> <template #header>
<span>{{ travel.ref }} - {{ travel.id }}</span> <span>{{ travel.ref }} - {{ travel.id }}</span>
</template> </template>
<template #header-right> <template #header-right>
<QBtn color="white" dense flat icon="more_vert" round size="md"> <QBtn color="white" dense flat icon="more_vert" round size="md">
<QTooltip> <QTooltip>

View File

@ -7,8 +7,8 @@ import VnConfirm from 'components/ui/VnConfirm.vue';
const quasar = useQuasar(); const quasar = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
const session = useSession(); const { getTokenMultimedia } = useSession();
const token = session.getToken(); const token = getTokenMultimedia();
const counters = ref({ const counters = ref({
alquilerBandeja: { count: 0, id: 96001, title: 'CC Bandeja', isTray: true }, alquilerBandeja: { count: 0, id: 96001, title: 'CC Bandeja', isTray: true },

View File

@ -22,7 +22,7 @@ const $props = defineProps({
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const { getToken } = useSession(); const { getTokenMultimedia } = useSession();
const entityId = computed(() => { const entityId = computed(() => {
return $props.id || route.params.id; return $props.id || route.params.id;
@ -56,7 +56,7 @@ const filter = {
const sip = computed(() => worker.value?.sip && worker.value.sip.extension); const sip = computed(() => worker.value?.sip && worker.value.sip.extension);
function getWorkerAvatar() { function getWorkerAvatar() {
const token = getToken(); const token = getTokenMultimedia();
return `/api/Images/user/160x160/${entityId.value}/download?access_token=${token}`; return `/api/Images/user/160x160/${entityId.value}/download?access_token=${token}`;
} }
const data = ref(useCardDescription()); const data = ref(useCardDescription());

View File

@ -0,0 +1,140 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { onMounted, ref, computed } from 'vue';
import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import useNotify from 'src/composables/useNotify.js';
import axios from 'axios';
import { useRole } from 'src/composables/useRole';
const route = useRoute();
const { t } = useI18n();
const { notify } = useNotify();
const { hasAny } = useRole();
const fetchCurrentDeviceRef = ref(null);
const deviceProductionsFilter = {
fields: ['id', 'serialNumber', 'modelFk'],
where: { stateFk: 'idle' },
order: 'id',
};
const deviceProductionsOptions = ref([]);
const newPDA = ref({});
const currentPDA = ref(null);
const isAllowedToEdit = computed(() => hasAny(['hr', 'productionAssi']));
const setCurrentPDA = (data) => {
currentPDA.value = data;
currentPDA.value.description = `ID: ${currentPDA.value.deviceProductionFk} ${t(
'worker.pda.model'
)}: ${currentPDA.value.deviceProduction.modelFk} ${t('worker.pda.serialNumber')}: ${
currentPDA.value.deviceProduction.serialNumber
}`;
};
const deallocatePDA = async (data) => {
try {
await axios.post(`Workers/${route.params.id}/deallocatePDA`, {
pda: currentPDA.value.deviceProductionFk,
});
data.pda = null;
currentPDA.value = null;
await fetchCurrentDeviceRef.value.fetch();
notify(t('PDA deallocated'), 'positive');
} catch (err) {
console.error('Error deallocating PDA');
}
};
onMounted(async () => await fetchCurrentDeviceRef.value.fetch());
</script>
<template>
<FetchData
url="DeviceProductions"
:filter="deviceProductionsFilter"
auto-load
@on-fetch="(data) => (deviceProductionsOptions = data)"
/>
<FetchData
ref="fetchCurrentDeviceRef"
url="DeviceProductionUsers"
:filter="{
where: { userFk: route.params.id },
include: { relation: 'deviceProduction' },
}"
auto-load
@on-fetch="(data) => setCurrentPDA(data[0])"
/>
<QPage class="column items-center q-pa-md">
<FormModel
url="DeviceProductionUsers"
:url-create="`Workers/${route.params.id}/allocatePDA`"
model="DeviceProductionUser"
:form-initial-data="newPDA"
auto-load
:default-buttons="{ save: { label: 'globals.assign', color: 'primary' } }"
@on-data-saved="(_, data) => setCurrentPDA(data)"
>
<template #form="{ data }">
<QField
v-if="currentPDA && currentPDA.description"
:label="t('worker.pda.currentPDA')"
:model-value="currentPDA.description"
:editable="false"
class="full-width"
>
<template #control>
<div tabindex="0">
{{ currentPDA.description }}
</div>
</template>
<template v-if="isAllowedToEdit" #append>
<QIcon
name="delete"
size="sm"
class="cursor-pointer"
color="primary"
@click="deallocatePDA(data)"
>
<QTooltip>
{{ t('worker.pda.removePDA') }}
</QTooltip>
</QIcon>
</template>
</QField>
<VnSelectFilter
v-else
:label="t('worker.pda.newPDA')"
v-model="data.pda"
:options="deviceProductionsOptions"
option-label="serialNumber"
option-value="id"
hide-selected
:disable="!isAllowedToEdit"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>ID: {{ scope.opt?.id }}</QItemLabel>
<QItemLabel caption>
{{ scope.opt?.modelFk }},
{{ scope.opt?.serialNumber }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</template>
</FormModel>
</QPage>
</template>
<i18n>
es:
PDA deallocated: PDA desasignada
</i18n>

View File

@ -6,6 +6,7 @@ export default {
meta: { meta: {
title: 'suppliers', title: 'suppliers',
icon: 'vn:supplier', icon: 'vn:supplier',
moduleName: 'Supplier',
}, },
component: RouterView, component: RouterView,
redirect: { name: 'SupplierMain' }, redirect: { name: 'SupplierMain' },

View File

@ -6,6 +6,7 @@ export default {
meta: { meta: {
title: 'claims', title: 'claims',
icon: 'vn:claims', icon: 'vn:claims',
moduleName: 'Claim',
}, },
component: RouterView, component: RouterView,
redirect: { name: 'ClaimMain' }, redirect: { name: 'ClaimMain' },

View File

@ -6,6 +6,7 @@ export default {
meta: { meta: {
title: 'customers', title: 'customers',
icon: 'vn:client', icon: 'vn:client',
moduleName: 'Customer',
}, },
component: RouterView, component: RouterView,
redirect: { name: 'CustomerMain' }, redirect: { name: 'CustomerMain' },

View File

@ -6,6 +6,7 @@ export default {
meta: { meta: {
title: 'entries', title: 'entries',
icon: 'vn:entry', icon: 'vn:entry',
moduleName: 'Entry',
}, },
component: RouterView, component: RouterView,
redirect: { name: 'EntryMain' }, redirect: { name: 'EntryMain' },

View File

@ -13,6 +13,7 @@ import Travel from './travel';
import Order from './order'; import Order from './order';
import Department from './department'; import Department from './department';
import Entry from './entry'; import Entry from './entry';
import roadmap from "./roadmap";
export default [ export default [
Item, Item,
@ -30,4 +31,5 @@ export default [
invoiceIn, invoiceIn,
Department, Department,
Entry, Entry,
roadmap
]; ];

View File

@ -6,6 +6,7 @@ export default {
meta: { meta: {
title: 'invoiceIns', title: 'invoiceIns',
icon: 'vn:invoice-in', icon: 'vn:invoice-in',
moduleName: 'InvoiceIn',
}, },
component: RouterView, component: RouterView,
redirect: { name: 'InvoiceInMain' }, redirect: { name: 'InvoiceInMain' },

View File

@ -6,6 +6,7 @@ export default {
meta: { meta: {
title: 'invoiceOuts', title: 'invoiceOuts',
icon: 'vn:invoice-out', icon: 'vn:invoice-out',
moduleName: 'InvoiceOut',
}, },
component: RouterView, component: RouterView,
redirect: { name: 'InvoiceOutMain' }, redirect: { name: 'InvoiceOutMain' },
@ -43,7 +44,7 @@ export default {
name: 'InvoiceOutNegativeBases', name: 'InvoiceOutNegativeBases',
meta: { meta: {
title: 'negativeBases', title: 'negativeBases',
icon: 'view_list', icon: 'vn:ticket',
}, },
component: () => component: () =>
import('src/pages/InvoiceOut/InvoiceOutNegativeBases.vue'), import('src/pages/InvoiceOut/InvoiceOutNegativeBases.vue'),

View File

@ -6,6 +6,7 @@ export default {
meta: { meta: {
title: 'order', title: 'order',
icon: 'vn:basket', icon: 'vn:basket',
moduleName: 'Order',
}, },
component: RouterView, component: RouterView,
redirect: { name: 'OrderMain' }, redirect: { name: 'OrderMain' },

View File

@ -0,0 +1,52 @@
import { RouterView } from 'vue-router';
export default {
path: '/route/roadmap',
name: 'Roadmap',
meta: {
title: 'roadmap',
icon: 'vn:troncales',
},
component: RouterView,
redirect: { name: 'RouteMain' },
menus: {
card: ['RoadmapBasicData', 'RoadmapStops'],
},
children: [
{
name: 'RouteRoadmapCard',
path: ':id',
component: () => import('src/pages/Route/Roadmap/RoadmapCard.vue'),
redirect: { name: 'RoadmapSummary' },
children: [
{
name: 'RoadmapSummary',
path: 'summary',
meta: {
title: 'summary',
icon: 'open_in_new',
},
component: () => import('pages/Route/Roadmap/RoadmapSummary.vue'),
},
{
name: 'RoadmapBasicData',
path: 'basic-data',
meta: {
title: 'basicData',
icon: 'vn:settings',
},
component: () => import('pages/Route/Roadmap/RoadmapBasicData.vue'),
},
{
name: 'RoadmapStops',
path: 'stops',
meta: {
title: 'stops',
icon: 'vn:lines',
},
component: () => import('pages/Route/Roadmap/RoadmapStops.vue'),
},
],
},
],
};

View File

@ -6,12 +6,13 @@ export default {
meta: { meta: {
title: 'routes', title: 'routes',
icon: 'vn:delivery', icon: 'vn:delivery',
moduleName: 'Route',
}, },
component: RouterView, component: RouterView,
redirect: { name: 'RouteMain' }, redirect: { name: 'RouteMain' },
menus: { menus: {
main: ['RouteList', 'CmrList'], main: ['RouteList', 'RouteAutonomous', 'RouteRoadmap', 'CmrList'],
card: ['RouteBasicData'], card: ['RouteBasicData', 'RouteTickets', 'RouteLog'],
}, },
children: [ children: [
{ {
@ -20,15 +21,6 @@ export default {
component: () => import('src/pages/Route/RouteMain.vue'), component: () => import('src/pages/Route/RouteMain.vue'),
redirect: { name: 'RouteList' }, redirect: { name: 'RouteList' },
children: [ children: [
{
path: 'cmr',
name: 'CmrList',
meta: {
title: 'cmrsList',
icon: 'fact_check',
},
component: () => import('src/pages/Route/Cmr/CmrList.vue'),
},
{ {
path: 'list', path: 'list',
name: 'RouteList', name: 'RouteList',
@ -46,6 +38,42 @@ export default {
}, },
component: () => import('src/pages/Route/Card/RouteForm.vue'), component: () => import('src/pages/Route/Card/RouteForm.vue'),
}, },
{
path: 'agency-term',
name: 'RouteAutonomous',
meta: {
title: 'autonomous',
icon: 'vn:agency-term',
},
component: () => import('src/pages/Route/RouteAutonomous.vue'),
},
{
path: 'roadmap',
name: 'RouteRoadmap',
meta: {
title: 'RouteRoadmap',
icon: 'vn:troncales',
},
component: () => import('src/pages/Route/RouteRoadmap.vue'),
},
{
path: 'roadmap/create',
name: 'RouteRoadmapCreate',
meta: {
title: 'RouteRoadmapCreate',
icon: 'vn:troncales',
},
component: () => import('src/pages/Route/Roadmap/RoadmapCreate.vue'),
},
{
path: 'cmr',
name: 'CmrList',
meta: {
title: 'cmrsList',
icon: 'fact_check',
},
component: () => import('src/pages/Route/Cmr/CmrList.vue'),
},
], ],
}, },
{ {
@ -72,6 +100,24 @@ export default {
}, },
component: () => import('pages/Route/Card/RouteSummary.vue'), component: () => import('pages/Route/Card/RouteSummary.vue'),
}, },
{
path: 'tickets',
name: 'RouteTickets',
meta: {
title: 'tickets',
icon: 'vn:ticket',
},
component: () => import('src/pages/Route/RouteTickets.vue'),
},
{
path: 'log',
name: 'RouteLog',
meta: {
title: 'log',
icon: 'vn:History',
},
component: () => import('src/pages/Route/RouteLog.vue'),
},
], ],
}, },
], ],

View File

@ -1,17 +1,18 @@
import {RouterView} from "vue-router"; import { RouterView } from 'vue-router';
export default { export default {
path: '/shelving', path: '/shelving',
name: 'Shelving', name: 'Shelving',
meta: { meta: {
title: 'shelving', title: 'shelving',
icon: 'vn:inventory' icon: 'vn:inventory',
moduleName: 'Shelving',
}, },
component: RouterView, component: RouterView,
redirect: { name: 'ShelvingMain' }, redirect: { name: 'ShelvingMain' },
menus: { menus: {
main: ['ShelvingList'], main: ['ShelvingList'],
card: ['ShelvingBasicData', 'ShelvingLog'] card: ['ShelvingBasicData', 'ShelvingLog'],
}, },
children: [ children: [
{ {
@ -51,8 +52,7 @@ export default {
meta: { meta: {
title: 'summary', title: 'summary',
}, },
component: () => component: () => import('pages/Shelving/Card/ShelvingSummary.vue'),
import('pages/Shelving/Card/ShelvingSummary.vue'),
}, },
{ {
name: 'ShelvingBasicData', name: 'ShelvingBasicData',
@ -75,6 +75,5 @@ export default {
}, },
], ],
}, },
] ],
}; };

View File

@ -6,6 +6,7 @@ export default {
meta: { meta: {
title: 'tickets', title: 'tickets',
icon: 'vn:ticket', icon: 'vn:ticket',
moduleName: 'Ticket',
}, },
component: RouterView, component: RouterView,
redirect: { name: 'TicketMain' }, redirect: { name: 'TicketMain' },

View File

@ -6,6 +6,7 @@ export default {
meta: { meta: {
title: 'travel', title: 'travel',
icon: 'local_airport', icon: 'local_airport',
moduleName: 'Travel',
}, },
component: RouterView, component: RouterView,
redirect: { name: 'TravelMain' }, redirect: { name: 'TravelMain' },

View File

@ -6,6 +6,7 @@ export default {
meta: { meta: {
title: 'wagons', title: 'wagons',
icon: 'vn:trolley', icon: 'vn:trolley',
moduleName: 'Wagon',
}, },
component: RouterView, component: RouterView,
redirect: { name: 'WagonMain' }, redirect: { name: 'WagonMain' },

View File

@ -6,12 +6,13 @@ export default {
meta: { meta: {
title: 'workers', title: 'workers',
icon: 'vn:worker', icon: 'vn:worker',
moduleName: 'Worker',
}, },
component: RouterView, component: RouterView,
redirect: { name: 'WorkerMain' }, redirect: { name: 'WorkerMain' },
menus: { menus: {
main: ['WorkerList', 'WorkerDepartment'], main: ['WorkerList', 'WorkerDepartment'],
card: ['WorkerNotificationsManager'], card: ['WorkerNotificationsManager', 'WorkerPda'],
departmentCard: ['BasicData'], departmentCard: ['BasicData'],
}, },
children: [ children: [
@ -75,6 +76,15 @@ export default {
component: () => component: () =>
import('src/pages/Worker/Card/WorkerNotificationsManager.vue'), import('src/pages/Worker/Card/WorkerNotificationsManager.vue'),
}, },
{
name: 'WorkerPda',
path: 'pda',
meta: {
title: 'pda',
icon: 'phone_android',
},
component: () => import('src/pages/Worker/Card/WorkerPda.vue'),
},
], ],
}, },
], ],

View File

@ -13,6 +13,7 @@ import department from './modules/department';
import shelving from 'src/router/modules/shelving'; import shelving from 'src/router/modules/shelving';
import order from 'src/router/modules/order'; import order from 'src/router/modules/order';
import entry from 'src/router/modules/entry'; import entry from 'src/router/modules/entry';
import roadmap from "src/router/modules/roadmap";
const routes = [ const routes = [
{ {
@ -66,6 +67,7 @@ const routes = [
supplier, supplier,
travel, travel,
department, department,
roadmap,
entry, entry,
{ {
path: '/:catchAll(.*)*', path: '/:catchAll(.*)*',

Some files were not shown because too many files have changed in this diff Show More