0
0
Fork 0

Merge branch 'dev' into 7190_renewTokenMultimedia

This commit is contained in:
Javier Segarra 2024-04-19 08:54:36 +00:00
commit bd01fcdd88
85 changed files with 4953 additions and 3012 deletions

View File

@ -5,10 +5,16 @@ 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).
## [2418.01]
## [2416.01] - 2024-04-18 ## [2416.01] - 2024-04-18
### Added ### Added
### Fixed
- (General) => Se vuelven a mostrar los parámetros en la url al aplicar un filtro
## [2414.01] - 2024-04-04 ## [2414.01] - 2024-04-04
### Added ### Added
@ -16,6 +22,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- (Tickets) => Se añade la opción de clonar ticket. #6951 - (Tickets) => Se añade la opción de clonar ticket. #6951
- (Parking) => Se añade la sección Parking. #5186 - (Parking) => Se añade la sección Parking. #5186
- (Rutas) => Se añade el campo "servida" a la tabla y se añade también a los filtros. #7130
### Changed ### Changed
### Fixed ### Fixed

View File

@ -1,6 +1,6 @@
{ {
"name": "salix-front", "name": "salix-front",
"version": "24.16.0", "version": "24.18.0",
"description": "Salix frontend", "description": "Salix frontend",
"productName": "Salix", "productName": "Salix",
"author": "Verdnatura", "author": "Verdnatura",
@ -32,6 +32,7 @@
"@intlify/unplugin-vue-i18n": "^0.8.1", "@intlify/unplugin-vue-i18n": "^0.8.1",
"@pinia/testing": "^0.1.2", "@pinia/testing": "^0.1.2",
"@quasar/app-vite": "^1.7.3", "@quasar/app-vite": "^1.7.3",
"@quasar/quasar-app-extension-qcalendar": "4.0.0-beta.15",
"@quasar/quasar-app-extension-testing-unit-vitest": "^0.4.0", "@quasar/quasar-app-extension-testing-unit-vitest": "^0.4.0",
"@vue/test-utils": "^2.4.4", "@vue/test-utils": "^2.4.4",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",

View File

@ -49,6 +49,9 @@ devDependencies:
'@quasar/app-vite': '@quasar/app-vite':
specifier: ^1.7.3 specifier: ^1.7.3
version: 1.7.3(eslint@8.56.0)(pinia@2.1.7)(quasar@2.14.5)(vue-router@4.2.5)(vue@3.4.19) version: 1.7.3(eslint@8.56.0)(pinia@2.1.7)(quasar@2.14.5)(vue-router@4.2.5)(vue@3.4.19)
'@quasar/quasar-app-extension-qcalendar':
specifier: 4.0.0-beta.15
version: 4.0.0-beta.15
'@quasar/quasar-app-extension-testing-unit-vitest': '@quasar/quasar-app-extension-testing-unit-vitest':
specifier: ^0.4.0 specifier: ^0.4.0
version: 0.4.0(@vue/test-utils@2.4.4)(quasar@2.14.5)(vite@5.1.4)(vitest@0.31.4)(vue@3.4.19) version: 0.4.0(@vue/test-utils@2.4.4)(quasar@2.14.5)(vite@5.1.4)(vitest@0.31.4)(vue@3.4.19)
@ -912,6 +915,13 @@ packages:
resolution: {integrity: sha512-SlOhwzXyPQHWgQIS2ncyDdYdksCJvUYNtgsDQqzAKEG3r3d/ejOxvThle79HTK3Q6HB+gQWFG21Ux00Osr5XSw==} resolution: {integrity: sha512-SlOhwzXyPQHWgQIS2ncyDdYdksCJvUYNtgsDQqzAKEG3r3d/ejOxvThle79HTK3Q6HB+gQWFG21Ux00Osr5XSw==}
dev: false dev: false
/@quasar/quasar-app-extension-qcalendar@4.0.0-beta.15:
resolution: {integrity: sha512-i6hQkcP70LXLfVMPZMKQjSg3681gjZmASV3vq6ULzc0LhtBiPneLdVNNtH2itkWxAmaUj+1heQDI5Pa0F7VKLQ==}
engines: {node: '>= 10.0.0', npm: '>= 5.6.0', yarn: '>= 1.6.0'}
dependencies:
'@quasar/quasar-ui-qcalendar': 4.0.0-beta.19
dev: true
/@quasar/quasar-app-extension-testing-unit-vitest@0.4.0(@vue/test-utils@2.4.4)(quasar@2.14.5)(vite@5.1.4)(vitest@0.31.4)(vue@3.4.19): /@quasar/quasar-app-extension-testing-unit-vitest@0.4.0(@vue/test-utils@2.4.4)(quasar@2.14.5)(vite@5.1.4)(vitest@0.31.4)(vue@3.4.19):
resolution: {integrity: sha512-eyzdUdmZiCueNS+5nedjMmzdbpCetSrtdGIwW6KplW1dTzRbLiNvYUjpBOxQGmJCgEhWy9zuswJ7MZ/bTql24Q==} resolution: {integrity: sha512-eyzdUdmZiCueNS+5nedjMmzdbpCetSrtdGIwW6KplW1dTzRbLiNvYUjpBOxQGmJCgEhWy9zuswJ7MZ/bTql24Q==}
engines: {node: '>= 12.22.1', npm: '>= 6.14.12', yarn: '>= 1.17.3'} engines: {node: '>= 12.22.1', npm: '>= 6.14.12', yarn: '>= 1.17.3'}
@ -939,6 +949,10 @@ packages:
- vite - vite
dev: true dev: true
/@quasar/quasar-ui-qcalendar@4.0.0-beta.19:
resolution: {integrity: sha512-BT0G2JjgKl1bqNrY5utcYeoy8gK+U9k3Pz1YDi1OB265W/jHU6nFoWMEUdY3JdvMccwkXTL2DLVyl3eqAUyLyg==}
dev: true
/@quasar/render-ssr-error@1.0.3: /@quasar/render-ssr-error@1.0.3:
resolution: {integrity: sha512-A8RF99q6/sOSe1Ighnh5syEIbliD3qUYEJd2HyfFyBPSMF+WYGXon5dmzg4nUoK662NgOggInevkDyBDJcZugg==} resolution: {integrity: sha512-A8RF99q6/sOSe1Ighnh5syEIbliD3qUYEJd2HyfFyBPSMF+WYGXon5dmzg4nUoK662NgOggInevkDyBDJcZugg==}
engines: {node: '>= 16'} engines: {node: '>= 16'}

View File

@ -93,13 +93,11 @@ module.exports = configure(function (/* ctx */) {
[ [
VueI18nPlugin({ VueI18nPlugin({
runtimeOnly: false, runtimeOnly: false,
include: [
path.resolve(__dirname, './src/i18n/locale/**'),
path.resolve(__dirname, './src/pages/**/locale/**'),
],
}), }),
{
// if you want to use Vue I18n Legacy API, you need to set `compositionOnly: false`
// compositionOnly: false,
// you need to set i18n resource including paths !
include: path.resolve(__dirname, './src/i18n/**'),
},
], ],
], ],
}, },

View File

@ -1,7 +1,6 @@
{ {
"@quasar/testing-unit-vitest": { "@quasar/testing-unit-vitest": {
"options": [ "options": ["scripts"]
"scripts" },
] "@quasar/qcalendar": {}
} }
}

View File

@ -101,16 +101,16 @@ onMounted(async () => {
}); });
onBeforeRouteLeave((to, from, next) => { onBeforeRouteLeave((to, from, next) => {
if (!hasChanges.value) next(); if (hasChanges.value)
quasar.dialog({
quasar.dialog({ component: VnConfirm,
component: VnConfirm, componentProps: {
componentProps: { title: t('Unsaved changes will be lost'),
title: t('Unsaved changes will be lost'), message: t('Are you sure exit without saving?'),
message: t('Are you sure exit without saving?'), promise: () => next(),
promise: () => next(), },
}, });
}); else next();
}); });
onUnmounted(() => { onUnmounted(() => {

View File

@ -78,6 +78,7 @@ defineExpose({
<div class="q-mt-lg row justify-end"> <div class="q-mt-lg row justify-end">
<QBtn <QBtn
:label="t('globals.save')" :label="t('globals.save')"
:title="t('globals.save')"
type="submit" type="submit"
color="primary" color="primary"
:disabled="isLoading" :disabled="isLoading"
@ -85,6 +86,7 @@ defineExpose({
/> />
<QBtn <QBtn
:label="t('globals.cancel')" :label="t('globals.cancel')"
:title="t('globals.cancel')"
type="reset" type="reset"
color="primary" color="primary"
flat flat

View File

@ -5,7 +5,7 @@ import { useCapitalize } from 'src/composables/useCapitalize';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
const props = defineProps({ const props = defineProps({
modelValue: { type: String, default: '' }, modelValue: { type: [String, Number], default: '' },
}); });
const { t } = useI18n(); const { t } = useI18n();

View File

@ -198,11 +198,13 @@ function addDefaultData(data) {
en: en:
contentTypesInfo: Allowed file types {allowedContentTypes} contentTypesInfo: Allowed file types {allowedContentTypes}
EntryDmsDescription: Reference {reference} EntryDmsDescription: Reference {reference}
WorkersDescription: Working of employee id {reference}
SupplierDmsDescription: Reference {reference} SupplierDmsDescription: Reference {reference}
es: es:
Generate identifier for original file: Generar identificador para archivo original Generate identifier for original file: Generar identificador para archivo original
contentTypesInfo: Tipos de archivo permitidos {allowedContentTypes} contentTypesInfo: Tipos de archivo permitidos {allowedContentTypes}
EntryDmsDescription: Referencia {reference} EntryDmsDescription: Referencia {reference}
WorkersDescription: Laboral del empleado {reference}
SupplierDmsDescription: Referencia {reference} SupplierDmsDescription: Referencia {reference}
</i18n> </i18n>

View File

@ -5,9 +5,11 @@ import { useRoute } from 'vue-router';
import { useQuasar, QCheckbox, QBtn, QInput } from 'quasar'; import { useQuasar, QCheckbox, QBtn, QInput } from 'quasar';
import axios from 'axios'; import axios from 'axios';
import FetchData from 'components/FetchData.vue'; import VnPaginate from 'components/ui/VnPaginate.vue';
import VnDms from 'src/components/common/VnDms.vue'; import VnDms from 'src/components/common/VnDms.vue';
import VnConfirm from 'components/ui/VnConfirm.vue'; import VnConfirm from 'components/ui/VnConfirm.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
import VnUserLink from '../ui/VnUserLink.vue';
import { downloadFile } from 'src/composables/downloadFile'; import { downloadFile } from 'src/composables/downloadFile';
const route = useRoute(); const route = useRoute();
@ -26,6 +28,15 @@ const $props = defineProps({
type: String, type: String,
default: null, default: null,
}, },
deleteModel: {
type: String,
default: null,
},
downloadModel: {
type: String,
required: false,
default: null,
},
defaultDmsCode: { defaultDmsCode: {
type: String, type: String,
required: true, required: true,
@ -74,7 +85,7 @@ const dmsFilter = {
], ],
}, },
}, },
order: ['dmsFk DESC'], where: { [$props.filter]: route.params.id },
}; };
const columns = computed(() => [ const columns = computed(() => [
@ -94,12 +105,12 @@ const columns = computed(() => [
props: (prop) => ({ props: (prop) => ({
readonly: true, readonly: true,
borderless: true, borderless: true,
'model-value': prop.row.dmsType.name, 'model-value': prop.row.dmsType?.name,
}), }),
}, },
{ {
align: 'left', align: 'left',
field: 'order', field: 'hardCopyNumber',
label: t('globals.order'), label: t('globals.order'),
name: 'order', name: 'order',
component: 'span', component: 'span',
@ -117,6 +128,7 @@ const columns = computed(() => [
label: t('globals.description'), label: t('globals.description'),
name: 'description', name: 'description',
component: 'span', component: 'span',
props: (prop) => ({ value: prop.value?.toUpperCase() }),
}, },
{ {
align: 'left', align: 'left',
@ -136,21 +148,53 @@ const columns = computed(() => [
name: 'file', name: 'file',
component: 'span', component: 'span',
}, },
{
align: 'left',
field: 'worker',
label: t('globals.worker'),
name: 'worker',
component: VnUserLink,
props: (prop) => ({
name: prop.row.worker?.user?.name.toLowerCase(),
workerId: prop.row.worker?.id,
}),
},
{
align: 'left',
field: 'created',
label: t('globals.created'),
name: 'created',
component: VnInputDate,
props: (prop) => ({
disable: true,
'model-value': prop.row.created,
}),
},
{ {
field: 'options', field: 'options',
name: 'options', name: 'options',
components: [ components: [
{ {
component: QBtn, component: QBtn,
name: 'download',
isDocuware: true,
props: () => ({ props: () => ({
icon: 'cloud_download', icon: 'cloud_download',
flat: true, flat: true,
color: 'primary', color: 'primary',
}), }),
click: (prop) => downloadFile(prop.row.id), click: (prop) =>
downloadFile(
prop.row.id,
$props.downloadModel,
null,
prop.row.download
),
}, },
{ {
component: QBtn, component: QBtn,
name: 'edit',
external: false,
props: () => ({ props: () => ({
icon: 'edit', icon: 'edit',
flat: true, flat: true,
@ -160,6 +204,8 @@ const columns = computed(() => [
}, },
{ {
component: QBtn, component: QBtn,
name: 'delete',
external: false,
props: () => ({ props: () => ({
icon: 'delete', icon: 'delete',
flat: true, flat: true,
@ -167,12 +213,24 @@ const columns = computed(() => [
}), }),
click: (prop) => deleteDms(prop.row.id), click: (prop) => deleteDms(prop.row.id),
}, },
{
component: QBtn,
name: 'open',
external: true,
props: () => ({
icon: 'open_in_new',
flat: true,
color: 'primary',
}),
click: (prop) => open(prop.row.url),
},
], ],
}, },
]); ]);
function setData(data) { function setData(data) {
const newData = data.map((value) => value.dms); const newData = data.map((value) => value.dms || value);
newData.sort((a, b) => new Date(b.created) - new Date(a.created));
rows.value = newData; rows.value = newData;
} }
@ -186,7 +244,7 @@ function deleteDms(dmsFk) {
}, },
}) })
.onOk(async () => { .onOk(async () => {
await axios.post(`${$props.model}/${dmsFk}/removeFile`); await axios.post(`${$props.deleteModel ?? $props.model}/${dmsFk}/removeFile`);
const index = rows.value.findIndex((row) => row.id == dmsFk); const index = rows.value.findIndex((row) => row.id == dmsFk);
rows.value.splice(index, 1); rows.value.splice(index, 1);
}); });
@ -206,84 +264,106 @@ function parseDms(data) {
} }
return data; return data;
} }
async function open(url) {
window.open(url).focus();
}
function shouldRenderButton(button, isExternal = false) {
if (button.name == 'download') return true;
return button.external === isExternal;
}
</script> </script>
<template> <template>
<FetchData <VnPaginate
ref="dmsRef" ref="dmsRef"
:data-key="$props.model"
:url="$props.model" :url="$props.model"
:filter="dmsFilter" :filter="dmsFilter"
:where="{ [$props.filter]: route.params.id }" :order="['dmsFk DESC']"
:auto-load="true"
@on-fetch="setData" @on-fetch="setData"
auto-load
/>
<QTable
:columns="columns"
:rows="rows"
class="full-width q-mt-md"
hide-bottom
row-key="clientFk"
:grid="$q.screen.lt.sm"
> >
<template #body-cell="props"> <template #body>
<QTd :props="props"> <QTable
<QTr :props="props"> :columns="columns"
<component :rows="rows"
v-if="props.col.component" class="full-width q-mt-md"
:is="props.col.component" hide-bottom
v-bind="props.col.props && props.col.props(props)" row-key="clientFk"
> :grid="$q.screen.lt.sm"
<span >
v-if="props.col.component == 'span'" <template #body-cell="props">
style="white-space: wrap" <QTd :props="props">
>{{ props.value }}</span <QTr :props="props">
> <component
</component> v-if="props.col.component"
</QTr> :is="props.col.component"
v-bind="props.col.props && props.col.props(props)"
<div class="flex justify-center" v-if="props.col.name == 'options'"> >
<div v-for="button of props.col.components" :key="button.id"> <span
<component v-if="props.col.component == 'span'"
:is="button.component" style="white-space: wrap"
v-bind="button.props(props)" >{{ props.value }}</span
@click="button.click(props)"
/>
</div>
</div>
</QTd>
</template>
<template #item="props">
<div class="q-pa-xs col-xs-12 col-sm-6 grid-style-transition">
<QCard
bordered
flat
@keyup.ctrl.enter.stop="claimDevelopmentForm?.saveChanges()"
>
<QSeparator />
<QList dense>
<QItem v-for="col in props.cols" :key="col.name">
<div v-if="col.name != 'options'" class="row">
<span class="labelColor">{{ col.label }}:</span>
<span>{{ col.value }}</span>
</div>
<div v-if="col.name == 'options'" class="row">
<div
v-for="button of col.components"
:key="button.id"
class="row"
> >
<component </component>
:is="button.component" </QTr>
v-bind="button.props(col)"
@click="button.click(col)" <div class="row no-wrap" v-if="props.col.name == 'options'">
/> <div v-for="button of props.col.components" :key="button.id">
</div> <component
v-if="
shouldRenderButton(button, props.row.isDocuware)
"
:is="button.component"
v-bind="button.props(props)"
@click="button.click(props)"
/>
</div> </div>
</QItem> </div>
</QList> </QTd>
</QCard> </template>
</div> <template #item="props">
<div class="q-pa-xs col-xs-12 col-sm-6 grid-style-transition">
<QCard
bordered
flat
@keyup.ctrl.enter.stop="claimDevelopmentForm?.saveChanges()"
>
<QSeparator />
<QList dense>
<QItem v-for="col in props.cols" :key="col.name">
<div v-if="col.name != 'options'" class="row">
<span class="labelColor">{{ col.label }}:</span>
<span>{{ col.value }}</span>
</div>
<div v-if="col.name == 'options'" class="row">
<div
v-for="button of col.components"
:key="button.id"
class="row"
>
<component
v-if="
shouldRenderButton(
button.name,
props.row.isDocuware
)
"
:is="button.component"
v-bind="button.props(col)"
@click="button.click(col)"
/>
</div>
</div>
</QItem>
</QList>
</QCard>
</div>
</template>
</QTable>
</template> </template>
</QTable> </VnPaginate>
<QDialog v-model="formDialog.show"> <QDialog v-model="formDialog.show">
<VnDms <VnDms
:model="updateModel ?? model" :model="updateModel ?? model"

View File

@ -26,7 +26,7 @@ const value = computed({
emit('update:modelValue', value); emit('update:modelValue', value);
}, },
}); });
const hover = ref(false);
const styleAttrs = computed(() => { const styleAttrs = computed(() => {
return $props.isOutlined return $props.isOutlined
? { ? {
@ -41,6 +41,10 @@ const onEnterPress = () => {
emit('keyup.enter'); emit('keyup.enter');
}; };
const handleValue = (val = null) => {
value.value = val;
};
const focus = () => { const focus = () => {
vnInputRef.value.focus(); vnInputRef.value.focus();
}; };
@ -51,20 +55,33 @@ defineExpose({
</script> </script>
<template> <template>
<QInput <div
ref="vnInputRef" @mouseover="hover = true"
v-model="value" @mouseleave="hover = false"
v-bind="{ ...$attrs, ...styleAttrs }"
type="text"
:class="{ required: $attrs.required }"
@keyup.enter="onEnterPress()"
:rules="$attrs.required ? [requiredFieldRule] : null" :rules="$attrs.required ? [requiredFieldRule] : null"
> >
<template v-if="$slots.prepend" #prepend> <QInput
<slot name="prepend" /> ref="vnInputRef"
</template> v-model="value"
<template v-if="$slots.append" #append> v-bind="{ ...$attrs, ...styleAttrs }"
<slot name="append" /> :type="$attrs.type"
</template> :class="{ required: $attrs.required }"
</QInput> @keyup.enter="onEnterPress()"
:clearable="false"
>
<template v-if="$slots.prepend" #prepend>
<slot name="prepend" />
</template>
<template #append>
<slot name="append" v-if="$slots.append" />
<QIcon
name="close"
size="xs"
v-if="hover && value"
@click="handleValue(null)"
></QIcon>
</template>
</QInput>
</div>
</template> </template>

View File

@ -1,7 +1,6 @@
<script setup> <script setup>
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import VnInput from 'components/common/VnInput.vue'; import isValidDate from 'filters/isValidDate';
import isValidDate from "filters/isValidDate";
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
@ -17,6 +16,8 @@ const props = defineProps({
default: false, default: false,
}, },
}); });
const hover = ref(false);
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
const joinDateAndTime = (date, time) => { const joinDateAndTime = (date, time) => {
@ -77,30 +78,39 @@ const styleAttrs = computed(() => {
</script> </script>
<template> <template>
<VnInput <div @mouseover="hover = true" @mouseleave="hover = false">
class="vn-input-date" <QInput
:model-value="displayDate(value)" class="vn-input-date"
v-bind="{ ...$attrs, ...styleAttrs }" readonly
readonly :model-value="displayDate(value)"
@click="isPopupOpen = true" v-bind="{ ...$attrs, ...styleAttrs }"
> @click="isPopupOpen = true"
<template #append> >
<QIcon name="event" class="cursor-pointer"> <template #append>
<QPopupProxy <QIcon
v-model="isPopupOpen" name="close"
cover size="xs"
transition-show="scale" v-if="hover && value"
transition-hide="scale" @click="onDateUpdate(null)"
:no-parent-event="props.readonly" ></QIcon>
> <QIcon name="event" class="cursor-pointer">
<QDate <QPopupProxy
:model-value="formatDate(value)" v-model="isPopupOpen"
@update:model-value="onDateUpdate" cover
/> transition-show="scale"
</QPopupProxy> transition-hide="scale"
</QIcon> :no-parent-event="props.readonly"
</template> >
</VnInput> <QDate
:today-btn="true"
:model-value="formatDate(value)"
@update:model-value="onDateUpdate"
/>
</QPopupProxy>
</QIcon>
</template>
</QInput>
</div>
</template> </template>
<style lang="scss"> <style lang="scss">

View File

@ -2,7 +2,6 @@
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import isValidDate from 'filters/isValidDate'; import isValidDate from 'filters/isValidDate';
import VnInput from "components/common/VnInput.vue";
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
@ -20,6 +19,7 @@ const props = defineProps({
}); });
const { t } = useI18n(); const { t } = useI18n();
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
const value = computed({ const value = computed({
get() { get() {
return props.modelValue; return props.modelValue;
@ -27,12 +27,7 @@ const value = computed({
set(value) { set(value) {
const [hours, minutes] = value.split(':'); const [hours, minutes] = value.split(':');
const date = new Date(props.modelValue); const date = new Date(props.modelValue);
date.setHours( date.setHours(Number.parseInt(hours) || 0, Number.parseInt(minutes) || 0, 0, 0);
Number.parseInt(hours) || 0,
Number.parseInt(minutes) || 0,
0,
0
);
emit('update:modelValue', value ? date.toISOString() : null); emit('update:modelValue', value ? date.toISOString() : null);
}, },
}); });
@ -71,7 +66,7 @@ const styleAttrs = computed(() => {
</script> </script>
<template> <template>
<VnInput <QInput
class="vn-input-time" class="vn-input-time"
readonly readonly
:model-value="formatTime(value)" :model-value="formatTime(value)"
@ -79,7 +74,7 @@ const styleAttrs = computed(() => {
@click="isPopupOpen = true" @click="isPopupOpen = true"
> >
<template #append> <template #append>
<QIcon name="event" class="cursor-pointer"> <QIcon name="schedule" class="cursor-pointer">
<QPopupProxy <QPopupProxy
v-model="isPopupOpen" v-model="isPopupOpen"
cover cover
@ -111,7 +106,7 @@ const styleAttrs = computed(() => {
</QPopupProxy> </QPopupProxy>
</QIcon> </QIcon>
</template> </template>
</VnInput> </QInput>
</template> </template>
<style lang="scss"> <style lang="scss">

View File

@ -47,7 +47,7 @@ const arrayData = useArrayData($props.dataKey || $props.module, {
skip: 0, skip: 0,
}); });
const { store } = arrayData; const { store } = arrayData;
const entity = computed(() => store.data); const entity = computed(() =>Array.isArray( store.data) ? store.data[0] : store.data);
const isLoading = ref(false); const isLoading = ref(false);
defineExpose({ defineExpose({
@ -67,7 +67,7 @@ async function getData() {
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); state.set($props.dataKey, data);
emit('onFetch', data); emit('onFetch', Array.isArray(data) ? data[0] : data);
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }

View File

@ -32,7 +32,7 @@ const arrayData = useArrayData(props.dataKey || route.meta.moduleName, {
skip: 0, skip: 0,
}); });
const { store } = arrayData; const { store } = arrayData;
const entity = computed(() => store.data); const entity = computed(() => Array.isArray(store.data) ? store.data[0] : store.data);
const isLoading = ref(false); const isLoading = ref(false);
defineExpose({ defineExpose({

View File

@ -0,0 +1,66 @@
<script setup>
import { computed } from 'vue';
import { useQuasar } from 'quasar';
import '@quasar/quasar-ui-qcalendar/src/QCalendarVariables.sass';
const $props = defineProps({
bordered: {
type: Boolean,
default: false,
},
transparentBackground: {
type: Boolean,
default: false,
},
});
const $q = useQuasar();
const containerClasses = computed(() => {
const classes = ['main-container-background'];
if ($props.bordered) classes.push('--bordered');
if ($props.transparentBackground) classes.push('transparent-background');
else classes.push($q.dark.isActive ? '--dark' : '--light');
return classes;
});
</script>
<template>
<div :class="containerClasses">
<div class="nav-container row"><slot name="header" /></div>
<slot name="calendar" />
</div>
</template>
<style lang="scss">
.main-container-background {
--calendar-current-background-dark: transparent;
&.--dark {
background-color: var(--calendar-background-dark);
}
&.--light {
background-color: var(--calendar-background);
}
&.--bordered {
border: 1px solid black;
}
}
.transparent-background {
--calendar-background-dark: transparent;
--calendar-background: transparent;
--calendar-outside-background-dark: transparent;
}
.nav-container {
display: flex;
align-items: center;
&.--bordered {
border: 1px solid black;
}
}
</style>

View File

@ -61,7 +61,6 @@ const props = defineProps({
}); });
const emit = defineEmits(['onFetch', 'onPaginate']); const emit = defineEmits(['onFetch', 'onPaginate']);
defineExpose({ fetch });
const isLoading = ref(false); const isLoading = ref(false);
const pagination = ref({ const pagination = ref({
sortBy: props.order, sortBy: props.order,
@ -78,6 +77,7 @@ const arrayData = useArrayData(props.dataKey, {
userParams: props.userParams, userParams: props.userParams,
exprBuilder: props.exprBuilder, exprBuilder: props.exprBuilder,
}); });
const hasMoreData = ref();
const store = arrayData.store; const store = arrayData.store;
onMounted(() => { onMounted(() => {
@ -91,6 +91,10 @@ watch(
} }
); );
const addFilter = async (filter, params) => {
await arrayData.addFilter({ filter, params });
};
async function fetch() { async function fetch() {
await arrayData.fetch({ append: false }); await arrayData.fetch({ append: false });
if (!arrayData.hasMoreData.value) { if (!arrayData.hasMoreData.value) {
@ -106,11 +110,10 @@ async function paginate() {
isLoading.value = true; isLoading.value = true;
await arrayData.loadMore(); await arrayData.loadMore();
if (!arrayData.hasMoreData.value) { if (!arrayData.hasMoreData.value) {
if (store.userParamsChanged) arrayData.hasMoreData.value = true; if (store.userParamsChanged) arrayData.hasMoreData.value = true;
store.userParamsChanged = false; store.userParamsChanged = false;
isLoading.value = false; endPagination();
return; return;
} }
@ -120,12 +123,14 @@ async function paginate() {
pagination.value.sortBy = sortBy; pagination.value.sortBy = sortBy;
pagination.value.descending = descending; pagination.value.descending = descending;
isLoading.value = false; endPagination();
}
function endPagination() {
isLoading.value = false;
emit('onFetch', store.data); emit('onFetch', store.data);
emit('onPaginate'); emit('onPaginate');
} }
async function onLoad(index, done) { async function onLoad(index, done) {
if (!store.data) { if (!store.data) {
return done(); return done();
@ -140,6 +145,8 @@ async function onLoad(index, done) {
if (store.userParamsChanged) isDone = !arrayData.hasMoreData.value; if (store.userParamsChanged) isDone = !arrayData.hasMoreData.value;
done(isDone); done(isDone);
} }
defineExpose({ fetch, addFilter });
</script> </script>
<template> <template>
@ -175,11 +182,12 @@ async function onLoad(index, done) {
</QCard> </QCard>
</div> </div>
</div> </div>
<QInfiniteScroll <QInfiniteScroll
v-if="store.data" v-if="store.data"
@load="onLoad" @load="onLoad"
:offset="offset" :offset="offset"
:disable="disableInfiniteScroll || !arrayData.hasMoreData.value" :disable="disableInfiniteScroll || !arrayData.hasMoreData"
class="full-width" class="full-width"
v-bind="$attrs" v-bind="$attrs"
> >
@ -188,6 +196,9 @@ async function onLoad(index, done) {
<QSpinner color="orange" size="md" /> <QSpinner color="orange" size="md" />
</div> </div>
</QInfiniteScroll> </QInfiniteScroll>
<div v-if="!isLoading && hasMoreData" class="w-full flex justify-center q-mt-md">
<QBtn color="primary" :label="t('Load more data')" @click="paginate()" />
</div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -204,4 +215,5 @@ async function onLoad(index, done) {
es: es:
No data to display: Sin datos que mostrar No data to display: Sin datos que mostrar
No results found: No se han encontrado resultados No results found: No se han encontrado resultados
Load more data: Cargar más resultados
</i18n> </i18n>

View File

@ -1,17 +1,17 @@
<template> <template>
<div id="row" class="q-gutter-md q-mb-md"> <div class="vn-row q-gutter-md q-mb-md">
<slot></slot> <slot></slot>
</div> </div>
</template> </template>
<style lang="scss" scopped> <style lang="scss" scopped>
#row { .vn-row {
display: flex; display: flex;
> * { > * {
flex: 1; flex: 1;
} }
} }
@media screen and (max-width: 800px) { @media screen and (max-width: 800px) {
#row { .vn-row {
flex-direction: column; flex-direction: column;
} }
} }

View File

@ -76,7 +76,7 @@ const removeNode = (node) => {
notify(t('department.departmentRemoved'), 'positive'); notify(t('department.departmentRemoved'), 'positive');
await fetchNodeLeaves(parentFk); await fetchNodeLeaves(parentFk);
} catch (err) { } catch (err) {
console.log('Error removing department'); console.error('Error removing department');
} }
}); });
}; };

View File

@ -1,5 +1,6 @@
<script setup> <script setup>
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
const $props = defineProps({ const $props = defineProps({

View File

@ -1,11 +1,11 @@
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import { getUrl } from './getUrl'; import { getUrl } from './getUrl';
const {getTokenMultimedia} = useSession(); const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia(); const token = getTokenMultimedia();
export async function downloadFile(dmsId) { export async function downloadFile(id, model = 'dms', urlPath = '/downloadFile', url) {
let appUrl = await getUrl('', 'lilium'); let appUrl = await getUrl('', 'lilium');
appUrl = appUrl.replace('/#/', ''); appUrl = appUrl.replace('/#/', '');
window.open(`${appUrl}/api/dms/${dmsId}/downloadFile?access_token=${token}`); window.open(url ?? `${appUrl}/api/${model}/${id}${urlPath}?access_token=${token}`);
} }

View File

@ -1,5 +1,5 @@
import { onMounted, ref, computed } from 'vue'; import { onMounted, ref, computed } from 'vue';
import { useRouter, useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import axios from 'axios'; import axios from 'axios';
import { useArrayDataStore } from 'stores/useArrayDataStore'; import { useArrayDataStore } from 'stores/useArrayDataStore';
import { buildFilter } from 'filters/filterPanel'; import { buildFilter } from 'filters/filterPanel';
@ -15,7 +15,6 @@ export function useArrayData(key, userOptions) {
const store = arrayDataStore.get(key); const store = arrayDataStore.get(key);
const hasMoreData = ref(false); const hasMoreData = ref(false);
const router = useRouter();
const route = useRoute(); const route = useRoute();
let canceller = null; let canceller = null;
@ -98,14 +97,14 @@ export function useArrayData(key, userOptions) {
const { limit } = filter; const { limit } = filter;
hasMoreData.value = response.data.length === limit; hasMoreData.value = response.data.length >= limit;
store.hasMoreData = hasMoreData.value;
if (append) { if (append) {
if (!store.data) store.data = []; if (!store.data) store.data = [];
for (const row of response.data) store.data.push(row); for (const row of response.data) store.data.push(row);
} else { } else {
store.data = response.data; store.data = response.data;
if (!document.querySelectorAll('[role="dialog"]')) if (!document.querySelectorAll('[role="dialog"]').length)
updateRouter && updateStateParams(); updateRouter && updateStateParams();
} }
@ -145,6 +144,8 @@ export function useArrayData(key, userOptions) {
store.userParams = userParams; store.userParams = userParams;
store.skip = 0; store.skip = 0;
store.filter.skip = 0; store.filter.skip = 0;
page.value = 1;
await fetch({ append: false }); await fetch({ append: false });
return { filter, params }; return { filter, params };
} }
@ -168,7 +169,7 @@ export function useArrayData(key, userOptions) {
} }
async function loadMore() { async function loadMore() {
if (!hasMoreData.value) return; if (!hasMoreData.value && !store.hasMoreData) return;
store.skip = store.limit * page.value; store.skip = store.limit * page.value;
page.value += 1; page.value += 1;
@ -188,11 +189,15 @@ export function useArrayData(key, userOptions) {
if (store.userParams && Object.keys(store.userParams).length !== 0) if (store.userParams && Object.keys(store.userParams).length !== 0)
query.params = JSON.stringify(store.userParams); query.params = JSON.stringify(store.userParams);
if (router) const url = new URL(window.location.href);
router.replace({ const { hash: currentHash } = url;
path: route.path, const [currentRoute] = currentHash.split('?');
query: query,
}); const params = new URLSearchParams();
for (const param in query) params.append(param, query[param]);
url.hash = currentRoute + '?' + params.toString();
window.history.pushState({}, '', url.hash);
} }
const totalRows = computed(() => (store.data && store.data.length) || 0); const totalRows = computed(() => (store.data && store.data.length) || 0);

View File

@ -3,31 +3,29 @@ import { useRole } from './useRole';
import { useUserConfig } from './useUserConfig'; import { useUserConfig } from './useUserConfig';
import axios from 'axios'; import axios from 'axios';
import useNotify from './useNotify'; import useNotify from './useNotify';
const TOKEN_MULTIMEDIA = 'tokenMultimedia';
const TOKEN = 'token';
export function useSession() { export function useSession() {
const { notify } = useNotify(); 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() { function getTokenMultimedia() {
const localTokenMultimedia = localStorage.getItem('token'); // Temporal const localTokenMultimedia = localStorage.getItem(TOKEN_MULTIMEDIA);
const sessionTokenMultimedia = sessionStorage.getItem('token'); // Temporal const sessionTokenMultimedia = sessionStorage.getItem(TOKEN_MULTIMEDIA);
return localTokenMultimedia || sessionTokenMultimedia || ''; return localTokenMultimedia || sessionTokenMultimedia || '';
} }
function setToken(data) { function setToken(data) {
if (data.keepLogin) { const storage = data.keepLogin ? localStorage : sessionStorage;
localStorage.setItem('token', data.token); storage.setItem(TOKEN, data.token);
localStorage.setItem('tokenMultimedia', data.tokenMultimedia); storage.setItem(TOKEN_MULTIMEDIA, data.tokenMultimedia);
} else {
sessionStorage.setItem('token', data.token);
sessionStorage.setItem('tokenMultimedia', data.tokenMultimedia);
}
} }
async function destroyToken(url, storage, key) { async function destroyToken(url, storage, key) {
if (storage.getItem(key)) { if (storage.getItem(key)) {
@ -71,8 +69,8 @@ export function useSession() {
} }
function isLoggedIn() { function isLoggedIn() {
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);
} }

View File

@ -1,5 +1,6 @@
// app global css in SCSS form // app global css in SCSS form
@import './icons.scss'; @import './icons.scss';
@import '@quasar/quasar-ui-qcalendar/src/QCalendarMonth.sass';
body.body--light { body.body--light {
--font-color: black; --font-color: black;
@ -159,4 +160,15 @@ input::-webkit-inner-spin-button {
appearance: none; appearance: none;
-webkit-appearance: none; -webkit-appearance: none;
-moz-appearance: none; -moz-appearance: none;
} }
// Clases para modificar el color de fecha seleccionada en componente QCalendarMonth
.q-dark div .q-calendar-mini .q-calendar-month__day.q-selected .q-calendar__button {
background-color: $primary !important;
color: white !important;
}
.q-calendar-mini .q-calendar-month__day.q-selected .q-calendar__button {
background-color: $primary !important;
color: white !important;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,28 @@
import en from './en'; const files = import.meta.glob(`./locale/*.yml`);
import es from './es'; const modules = import.meta.glob(`../pages/**/locale/*.yml`);
export const localeEquivalence = {
'en':'en-GB' const translations = {};
for (const file in files) {
const lang = file.split('/').at(2).split('.')[0];
files[file]()
.then((g) => {
translations[lang] = g.default;
})
.finally(() => {
const actualLang = lang + '.yml';
for (const module in modules) {
if (!module.endsWith(actualLang)) continue;
modules[module]().then((t) => {
Object.assign(translations[lang], t.default);
})
}
});
} }
export default {
en: en, export const localeEquivalence = {
es: es, en: 'en-GB',
}; };
export default translations;

1180
src/i18n/locale/en.yml Normal file

File diff suppressed because it is too large Load Diff

1179
src/i18n/locale/es.yml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@ import ClaimNotes from 'src/pages/Claim/Card/ClaimNotes.vue';
import VnUserLink from 'src/components/ui/VnUserLink.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import VnTitle from 'src/components/common/VnTitle.vue'; import VnTitle from 'src/components/common/VnTitle.vue';
import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
@ -177,7 +178,7 @@ function openDialog(dmsId) {
@on-fetch="getClaimDms" @on-fetch="getClaimDms"
> >
<template #header="{ entity: { claim } }"> <template #header="{ entity: { claim } }">
{{ claim.id }} - {{ claim.client.name }} {{ claim.id }} - {{ claim.client.name }} ({{ claim.client.id }})
</template> </template>
<template #body="{ entity: { claim, salesClaimed, developments } }"> <template #body="{ entity: { claim, salesClaimed, developments } }">
<QCard class="vn-one"> <QCard class="vn-one">
@ -214,10 +215,10 @@ function openDialog(dmsId) {
</VnLv> </VnLv>
<VnLv :label="t('claim.summary.customer')"> <VnLv :label="t('claim.summary.customer')">
<template #value> <template #value>
<VnUserLink <span class="link cursor-pointer">
:name="claim.client?.name" {{ claim.client?.name }}
:worker-id="claim.client?.id" <CustomerDescriptorProxy :id="claim.clientFk" />
/> </span>
</template> </template>
</VnLv> </VnLv>
<VnLv <VnLv

View File

@ -48,13 +48,17 @@ const zones = ref();
</QItem> </QItem>
<QItem class="q-mb-sm"> <QItem class="q-mb-sm">
<QItemSection> <QItemSection>
<VnInput :label="t('Name')" v-model="params.name" is-outlined /> <VnInput
:label="t('customerFilter.filter.name')"
v-model="params.name"
is-outlined
/>
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem class="q-mb-sm"> <QItem class="q-mb-sm">
<QItemSection> <QItemSection>
<VnInput <VnInput
:label="t('Social Name')" :label="t('customerFilter.filter.socialName')"
v-model="params.socialName" v-model="params.socialName"
is-outlined is-outlined
/> />
@ -170,8 +174,6 @@ en:
params: params:
search: Contains search: Contains
fi: FI fi: FI
name: Name
socialName: Social Name
salesPersonFk: Salesperson salesPersonFk: Salesperson
provinceFk: Province provinceFk: Province
city: City city: City
@ -183,8 +185,6 @@ es:
params: params:
search: Contiene search: Contiene
fi: NIF fi: NIF
name: Nombre
socialName: Razón Social
salesPersonFk: Comercial salesPersonFk: Comercial
provinceFk: Provincia provinceFk: Provincia
city: Ciudad city: Ciudad
@ -193,8 +193,6 @@ es:
zoneFk: Zona zoneFk: Zona
postcode: CP postcode: CP
FI: NIF FI: NIF
Name: Nombre
Social Name: Razón social
Salesperson: Comercial Salesperson: Comercial
Province: Provincia Province: Provincia
City: Ciudad City: Ciudad

View File

@ -0,0 +1,4 @@
customerFilter:
filter:
name: Name
socialName: Social name

View File

@ -0,0 +1,4 @@
customerFilter:
filter:
name: Nombre
socialName: Razón Social

View File

@ -63,7 +63,7 @@ const removeDepartment = () => {
router.push({ name: 'WorkerDepartment' }); router.push({ name: 'WorkerDepartment' });
notify('department.departmentRemoved', 'positive'); notify('department.departmentRemoved', 'positive');
} catch (err) { } catch (err) {
console.log('Error removing department'); console.error('Error removing department');
} }
}); });
}; };

View File

@ -2,6 +2,7 @@
import { ref } from 'vue'; import { ref } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRole } from 'src/composables/useRole';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue'; import FormModel from 'components/FormModel.vue';
@ -15,6 +16,8 @@ import { toDate } from 'src/filters';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const { hasAny } = useRole();
const isAdministrative = () => hasAny(['administrative']);
const suppliersOptions = ref([]); const suppliersOptions = ref([]);
const travelsOptions = ref([]); const travelsOptions = ref([]);
@ -206,6 +209,7 @@ const onFilterTravelSelected = (formData, id) => {
</div> </div>
<div class="col"> <div class="col">
<QCheckbox <QCheckbox
v-if="isAdministrative()"
v-model="data.isBooked" v-model="data.isBooked"
:label="t('entry.basicData.booked')" :label="t('entry.basicData.booked')"
/> />

View File

@ -268,16 +268,11 @@ const importBuys = () => {
const toggleGroupingMode = async (buy, mode) => { const toggleGroupingMode = async (buy, mode) => {
try { try {
const grouping = 1; const groupingMode = mode === 'grouping' ? mode : 'packing';
const packing = 2; const newGroupingMode = buy.groupingMode === groupingMode ? null : groupingMode;
const groupingMode = mode === 'grouping' ? grouping : packing;
const newGroupingMode = buy.groupingMode === groupingMode ? 0 : groupingMode;
const params = { const params = {
groupingMode: newGroupingMode, groupingMode: newGroupingMode,
}; };
await axios.patch(`Buys/${buy.id}`, params); await axios.patch(`Buys/${buy.id}`, params);
buy.groupingMode = newGroupingMode; buy.groupingMode = newGroupingMode;
} catch (err) { } catch (err) {
@ -287,9 +282,9 @@ const toggleGroupingMode = async (buy, mode) => {
const lockIconType = (groupingMode, mode) => { const lockIconType = (groupingMode, mode) => {
if (mode === 'packing') { if (mode === 'packing') {
return groupingMode === 2 ? 'lock' : 'lock_open'; return groupingMode === 'packing' ? 'lock' : 'lock_open';
} else { } else {
return groupingMode === 1 ? 'lock' : 'lock_open'; return groupingMode === 'grouping' ? 'lock' : 'lock_open';
} }
}; };
</script> </script>

View File

@ -1,19 +1,19 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue'; import { ref, computed, onMounted } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import CrudModel from 'components/CrudModel.vue'; import CrudModel from 'components/CrudModel.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue'; import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
const route = useRoute(); const { params } = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const entryObservationsRef = ref(null); const entryObservationsRef = ref(null);
const entryObservationsOptions = ref([]); const entryObservationsOptions = ref([]);
const selected = ref([]);
const sortEntryObservationOptions = (data) => { const sortEntryObservationOptions = (data) => {
entryObservationsOptions.value = [...data].sort((a, b) => entryObservationsOptions.value = [...data].sort((a, b) =>
@ -24,6 +24,29 @@ const sortEntryObservationOptions = (data) => {
onMounted(() => { onMounted(() => {
if (entryObservationsRef.value) entryObservationsRef.value.reload(); if (entryObservationsRef.value) entryObservationsRef.value.reload();
}); });
const columns = computed(() => [
{
name: 'observationType',
label: t('entry.notes.observationType'),
field: (row) => row.observationTypeFk,
sortable: true,
options: entryObservationsOptions.value,
required: true,
model: 'observationTypeFk',
optionValue: 'id',
optionLabel: 'description',
tabIndex: 1,
align: 'left',
},
{
name: 'description',
label: t('globals.description'),
field: (row) => row.description,
tabIndex: 2,
align: 'left',
},
]);
</script> </script>
<template> <template>
<FetchData <FetchData
@ -37,65 +60,90 @@ onMounted(() => {
model="EntryAccount" model="EntryAccount"
:filter="{ :filter="{
fields: ['id', 'entryFk', 'observationTypeFk', 'description'], fields: ['id', 'entryFk', 'observationTypeFk', 'description'],
where: { entryFk: route.params.id }, where: { entryFk: params.id },
}" }"
ref="entryObservationsRef" ref="entryObservationsRef"
:default-remove="false" :data-required="{ entryFk: params.id }"
:data-required="{ entryFk: route.params.id }" v-model:selected="selected"
> >
<template #body="{ rows, validate }"> <template #body="{ rows, validate }">
<QCard class="q-pa-md"> <QTable
<VnRow v-model:selected="selected"
v-for="(row, index) in rows" :columns="columns"
:key="index" :rows="rows"
class="row q-gutter-md q-mb-md" :pagination="{ rowsPerPage: 0 }"
> row-key="$index"
<VnSelectFilter selection="multiple"
:label="t('entry.notes.observationType')" hide-pagination
v-model="row.observationTypeFk" :grid="$q.screen.lt.md"
:options="entryObservationsOptions" table-header-class="text-left"
:disable="!!row.id" >
option-label="description" <template #body-cell-observationType="{ row, col }">
option-value="id" <QTd>
hide-selected <VnSelectFilter
/> v-model="row[col.model]"
:options="col.options"
<VnInput :option-value="col.optionValue"
:label="t('globals.description')" :option-label="col.optionLabel"
v-model="row.description" :autofocus="col.tabIndex == 1"
:rules="validate('EntryObservation.description')" input-debounce="0"
/> hide-selected
:required="true"
<div class="row justify-center items-center"> />
<QIcon </QTd>
name="delete" </template>
size="sm" <template #body-cell-description="{ row, col }">
class="cursor-pointer" <QTd>
color="primary" <VnInput
@click="entryObservationsRef.remove([row])" :label="t('globals.description')"
> v-model="row[col.name]"
<QTooltip> :rules="validate('EntryObservation.description')"
{{ t('Remove note') }} />
</QTooltip> </QTd>
</QIcon> </template>
<template #item="props">
<div class="q-pa-xs col-xs-12 col-sm-6 grid-style-transition">
<QCard bordered flat>
<QCardSection>
<QCheckbox v-model="props.selected" dense />
</QCardSection>
<QSeparator />
<QList dense>
<QItem>
<QItemSection>
<VnSelectFilter
v-model="props.row.observationTypeFk"
:options="entryObservationsOptions"
option-value="id"
option-label="description"
input-debounce="0"
hide-selected
:required="true"
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInput
:label="t('globals.description')"
v-model="props.row.description"
:rules="
validate('EntryObservation.description')
"
/>
</QItemSection>
</QItem>
</QList>
</QCard>
</div> </div>
</VnRow> </template>
<QIcon </QTable>
name="add"
size="sm"
class="cursor-pointer"
color="primary"
@click="entryObservationsRef.insert()"
>
<QTooltip>
{{ t('Add note') }}
</QTooltip>
</QIcon>
</QCard>
</template> </template>
</CrudModel> </CrudModel>
<QPageSticky position="bottom-right" :offset="[25, 25]">
<QBtn fab color="primary" icon="add" @click="entryObservationsRef.insert()" />
</QPageSticky>
</template> </template>
<i18n> <i18n>
es: es:
Add note: Añadir nota Add note: Añadir nota

View File

@ -1,6 +1,8 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { onMounted } from 'vue';
import { useStateStore } from 'stores/useStateStore';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue'; import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
@ -19,6 +21,11 @@ const props = defineProps({
const currenciesOptions = ref([]); const currenciesOptions = ref([]);
const companiesOptions = ref([]); const companiesOptions = ref([]);
const suppliersOptions = ref([]); const suppliersOptions = ref([]);
const stateStore = useStateStore();
onMounted(async () => {
stateStore.rightDrawer = true;
});
</script> </script>
<template> <template>
@ -58,7 +65,7 @@ const suppliersOptions = ref([]);
<QItemSection> <QItemSection>
<VnInput <VnInput
v-model="params.search" v-model="params.search"
:label="t('params.search')" :label="t('entryFilter.filter.search')"
is-outlined is-outlined
/> />
</QItemSection> </QItemSection>
@ -67,7 +74,7 @@ const suppliersOptions = ref([]);
<QItemSection> <QItemSection>
<VnInput <VnInput
v-model="params.reference" v-model="params.reference"
:label="t('params.reference')" :label="t('entryFilter.filter.reference')"
is-outlined is-outlined
/> />
</QItemSection> </QItemSection>
@ -210,8 +217,7 @@ const suppliersOptions = ref([]);
<i18n> <i18n>
en: en:
params: params:
search: General search
reference: Reference
invoiceNumber: Invoice number invoiceNumber: Invoice number
travelFk: Travel travelFk: Travel
companyFk: Company companyFk: Company
@ -225,8 +231,7 @@ en:
isOrdered: Ordered isOrdered: Ordered
es: es:
params: params:
search: Búsqueda general
reference: Referencia
invoiceNumber: Núm. factura invoiceNumber: Núm. factura
travelFk: Envío travelFk: Envío
companyFk: Empresa companyFk: Empresa

View File

@ -0,0 +1,8 @@
entryList:
list:
inventoryEntry: Inventory entry
virtualEntry: Virtual entry
entryFilter:
filter:
search: General search
reference: Reference

View File

@ -0,0 +1,8 @@
entryList:
list:
inventoryEntry: Es inventario
virtualEntry: Es una redada
entryFilter:
filter:
search: Búsqueda general
reference: Referencia

View File

@ -257,7 +257,7 @@ const requiredFieldRule = (val) => val || t('globals.requiredField');
const isAdministrative = () => hasAny(['administrative']); const isAdministrative = () => hasAny(['administrative']);
const isAgricultural = () => const isAgricultural = () =>
invoiceIn.value.supplier.sageWithholdingFk == config.value[0].sageWithholdingFk; invoiceIn.value?.supplier?.sageWithholdingFk === config.value[0]?.sageWithholdingFk;
function showPdfInvoice() { function showPdfInvoice() {
if (isAgricultural()) openReport(`InvoiceIns/${entityId.value}/invoice-in-pdf`); if (isAgricultural()) openReport(`InvoiceIns/${entityId.value}/invoice-in-pdf`);

View File

@ -32,6 +32,13 @@ const invoiceInFormRef = ref();
const expensesRef = ref(); const expensesRef = ref();
const newExpenseRef = ref(); const newExpenseRef = ref();
defineProps({
actionIcon: {
type: String,
default: 'add',
},
});
const columns = computed(() => [ const columns = computed(() => [
{ {
name: 'expense', name: 'expense',
@ -207,17 +214,16 @@ async function addExpense() {
@click.stop="value = null" @click.stop="value = null"
class="cursor-pointer" class="cursor-pointer"
/> />
<QBtn <QIcon
padding="xs" @click.stop.prevent="newExpenseRef.show()"
round :name="actionIcon"
flat size="xs"
icon="add_circle" class="default-icon"
@click.stop="newExpenseRef.show()"
> >
<QTooltip> <QTooltip>
{{ t('Create expense') }} {{ t('Create expense') }}
</QTooltip> </QTooltip>
</QBtn> </QIcon>
</template> </template>
</VnSelectFilter> </VnSelectFilter>
</QTd> </QTd>
@ -470,6 +476,11 @@ async function addExpense() {
.q-item { .q-item {
min-height: 0; min-height: 0;
} }
.default-icon {
cursor: pointer;
border-radius: 50px;
background-color: $primary;
}
</style> </style>
<i18n> <i18n>
es: es:

View File

@ -0,0 +1 @@
<template>Item basic data</template>

View File

@ -0,0 +1,170 @@
<script setup>
import { reactive, ref, onBeforeMount } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import FetchData from 'components/FetchData.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import axios from 'axios';
const { t } = useI18n();
const router = useRouter();
const newItemForm = reactive({});
const originsOptions = ref([]);
const tagsOptions = ref([]);
const validPriorities = ref([]);
const itemTypesOptions = ref([]);
const intrastatsOptions = ref([]);
const fetchDefaultPriorityTag = async () => {
const filter = {
fields: ['defaultPriority', 'defaultTag', 'validPriorities'],
limit: 1,
};
const { data } = await axios.get(`ItemConfigs`, { filter });
if (!data) return;
const dataRow = data[0];
validPriorities.value = [...dataRow.validPriorities];
newItemForm.priority = dataRow.defaultPriority;
newItemForm.tag = dataRow.defaultTag;
};
const redirectToItemBasicData = (_, { id }) => {
router.push({ name: 'ItemBasicData', params: { id } });
};
onBeforeMount(async () => {
await fetchDefaultPriorityTag();
});
</script>
<template>
<FetchData
url="Origins"
@on-fetch="(data) => (originsOptions = data)"
:filter="{ order: 'name' }"
auto-load
/>
<FetchData
url="Tags"
:filter="{ fields: ['id', 'name'], order: 'name ASC' }"
@on-fetch="(data) => (tagsOptions = data)"
auto-load
/>
<FetchData
url="ItemTypes"
:filter="{
fields: ['id', 'code', 'categoryFk', 'name'],
include: 'category',
order: 'name ASC',
}"
@on-fetch="(data) => (itemTypesOptions = data)"
auto-load
/>
<FetchData
url="Intrastats"
:filter="{ fields: ['id', 'description'], order: 'description ASC' }"
@on-fetch="(data) => (intrastatsOptions = data)"
auto-load
/>
<QPage>
<VnSubToolbar />
<FormModel
url-create="Items/new"
model="item"
:form-initial-data="newItemForm"
observe-form-changes
@on-data-saved="redirectToItemBasicData"
>
<template #form="{ data }">
<VnRow class="row q-gutter-md q-mb-md">
<VnInput
v-model="data.provisionalName"
:label="t('item.create.name')"
/>
<VnSelectFilter
:label="t('item.create.tag')"
v-model="data.tag"
:options="tagsOptions"
option-value="id"
option-label="name"
hide-selected
/>
<VnSelectFilter
:label="t('item.create.priority')"
v-model="data.priority"
:options="validPriorities"
option-value="priority"
option-label="priority"
hide-selected
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnSelectFilter
:label="t('item.create.type')"
v-model="data.typeFk"
:options="itemTypesOptions"
option-label="name"
option-value="id"
hide-selected
>
<template #option="scope">
<QItem class="column" v-bind="scope.itemProps">
<QItemLabel class="row">
<span style="width: 3em">
{{ scope.opt?.code }}
</span>
<span>
{{ scope.opt?.name }}
</span>
</QItemLabel>
<QItemLabel v-if="scope.opt?.category" caption>
{{ scope.opt?.category?.name }}
</QItemLabel>
</QItem>
</template>
</VnSelectFilter>
<VnSelectFilter
:label="t('item.create.intrastat')"
v-model="data.intrastatFk"
:options="intrastatsOptions"
option-label="description"
option-value="id"
hide-selected
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
{{ scope.opt?.description }}
</QItemLabel>
<QItemLabel caption>
#{{ scope.opt?.id }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnSelectFilter
:label="t('item.create.origin')"
v-model="data.originFk"
:options="originsOptions"
option-value="id"
option-label="name"
hide-selected
/>
</VnRow>
</template>
</FormModel>
</QPage>
</template>

View File

@ -1 +1,577 @@
<template>Item list</template> <script setup>
import { onMounted, ref, computed, reactive, onUnmounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import FetchData from 'components/FetchData.vue';
import FetchedTags from 'components/ui/FetchedTags.vue';
import TableVisibleColumns from 'src/components/common/TableVisibleColumns.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import ItemDescriptorProxy from '../Item/Card/ItemDescriptorProxy.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import ItemSummary from '../Item/Card/ItemSummary.vue';
import VnPaginate from 'components/ui/VnPaginate.vue';
import { useStateStore } from 'stores/useStateStore';
import { toDateFormat } from 'src/filters/date.js';
import { useSession } from 'composables/useSession';
import { dashIfEmpty } from 'src/filters';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { useVnConfirm } from 'composables/useVnConfirm';
import axios from 'axios';
const router = useRouter();
const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia();
const stateStore = useStateStore();
const { t } = useI18n();
const { viewSummary } = useSummaryDialog();
const { openConfirmationModal } = useVnConfirm();
const paginateRef = ref(null);
const itemTypesOptions = ref([]);
const originsOptions = ref([]);
const buyersOptions = ref([]);
const intrastatOptions = ref([]);
const itemCategoriesOptions = ref([]);
const visibleColumns = ref([]);
const allColumnNames = ref([]);
const exprBuilder = (param, value) => {
switch (param) {
case 'category':
return { 'ic.name': value };
case 'buyerFk':
return { 'it.workerFk': value };
case 'grouping':
return { 'b.grouping': value };
case 'packing':
return { 'b.packing': value };
case 'origin':
return { 'ori.code': value };
case 'typeFk':
return { 'i.typeFk': value };
case 'intrastat':
return { 'intr.description': value };
case 'name':
return { 'i.name': { like: `%${value}%` } };
case 'producer':
return { 'pr.name': { like: `%${value}%` } };
case 'id':
case 'size':
case 'subname':
case 'isActive':
case 'weightByPiece':
case 'stemMultiplier':
case 'stems':
return { [`i.${param}`]: value };
}
};
const params = reactive({});
const applyColumnFilter = async (col) => {
try {
const paramKey = col.columnFilter?.filterParamKey || col.field;
params[paramKey] = col.columnFilter.filterValue;
await paginateRef.value.addFilter(null, params);
} catch (err) {
console.error('Error applying column filter', err);
}
};
const getInputEvents = (col) => {
return col.columnFilter.type === 'select'
? { 'update:modelValue': () => applyColumnFilter(col) }
: {
'keyup.enter': () => applyColumnFilter(col),
};
};
const columns = computed(() => [
{
label: '',
name: 'picture',
align: 'left',
columnFilter: null,
},
{
label: t('item.list.id'),
name: 'id',
field: 'id',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('item.list.grouping'),
field: 'grouping',
name: 'grouping',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
label: t('item.list.packing'),
field: 'packing',
name: 'packing',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
label: t('globals.description'),
field: 'name',
name: 'description',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('item.list.stems'),
field: 'stems',
name: 'stems',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('item.list.size'),
field: 'size',
name: 'size',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('item.list.typeName'),
field: 'typeName',
name: 'typeName',
align: 'left',
sortable: true,
columnFilter: {
component: VnSelectFilter,
filterParamKey: 'typeFk',
type: 'select',
filterValue: null,
event: getInputEvents,
attrs: {
options: itemTypesOptions.value,
'option-value': 'id',
'option-label': 'name',
dense: true,
},
},
},
{
label: t('item.list.category'),
field: 'category',
name: 'category',
align: 'left',
sortable: true,
columnFilter: {
component: VnSelectFilter,
type: 'select',
filterValue: null,
event: getInputEvents,
attrs: {
options: itemCategoriesOptions.value,
'option-value': 'name',
'option-label': 'name',
dense: true,
},
},
},
{
label: t('item.list.intrastat'),
field: 'intrastat',
name: 'intrastat',
align: 'left',
sortable: true,
columnFilter: {
component: VnSelectFilter,
type: 'select',
filterValue: null,
event: getInputEvents,
attrs: {
options: intrastatOptions.value,
'option-value': 'description',
'option-label': 'description',
dense: true,
},
},
},
{
label: t('item.list.origin'),
field: 'origin',
name: 'origin',
align: 'left',
sortable: true,
columnFilter: {
component: VnSelectFilter,
type: 'select',
filterValue: null,
event: getInputEvents,
attrs: {
options: originsOptions.value,
'option-value': 'code',
'option-label': 'code',
dense: true,
},
},
},
{
label: t('item.list.userName'),
field: 'userName',
name: 'userName',
align: 'left',
sortable: true,
columnFilter: {
component: VnSelectFilter,
filterParamKey: 'buyerFk',
type: 'select',
filterValue: null,
event: getInputEvents,
attrs: {
options: buyersOptions.value,
'option-value': 'id',
'option-label': 'nickname',
dense: true,
},
},
},
{
label: t('item.list.weightByPiece'),
field: 'weightByPiece',
name: 'weightByPiece',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
label: t('item.list.stemMultiplier'),
field: 'stemMultiplier',
name: 'stemMultiplier',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
label: t('item.list.isActive'),
field: 'isActive',
name: 'isActive',
align: 'left',
sortable: true,
columnFilter: null,
},
{
label: t('item.list.producer'),
field: 'producer',
name: 'producer',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
label: t('item.list.landed'),
field: 'landed',
name: 'landed',
align: 'left',
sortable: true,
format: (val) => dashIfEmpty(toDateFormat(val)),
columnFilter: null,
},
{
label: '',
name: 'actions',
align: 'left',
columnFilter: null,
},
]);
const redirectToItemCreate = () => {
router.push({ name: 'ItemCreate' });
};
const redirectToItemSummary = (id) => {
router.push({ name: 'ItemSummary', params: { id } });
};
const cloneItem = async (itemFk) => {
try {
const { data } = await axios.post(`Items/${itemFk}/clone`);
if (!data) return;
router.push({ name: 'ItemTags', params: { id: data.id } });
} catch (err) {
console.error('Error cloning item', err);
}
};
onMounted(async () => {
stateStore.rightDrawer = true;
const filteredColumns = columns.value.filter(
(col) => col.name !== 'picture' && col.name !== 'actions'
);
allColumnNames.value = filteredColumns.map((col) => col.name);
});
onUnmounted(() => (stateStore.rightDrawer = false));
</script>
<template>
<FetchData
url="ItemTypes"
:filter="{ fields: ['id', 'name'], order: 'name ASC' }"
auto-load
@on-fetch="(data) => (itemTypesOptions = data)"
/>
<FetchData
url="ItemCategories"
:filter="{ fields: ['name'], order: 'name ASC' }"
auto-load
@on-fetch="(data) => (itemCategoriesOptions = data)"
/>
<FetchData
url="Intrastats"
:filter="{ fields: ['description'], order: 'description ASC' }"
auto-load
@on-fetch="(data) => (intrastatOptions = data)"
/>
<FetchData
url="Origins"
:filter="{ fields: ['code'], order: 'code ASC' }"
auto-load
@on-fetch="(data) => (originsOptions = data)"
/>
<FetchData
url="TicketRequests/getItemTypeWorker"
:filter="{ fields: ['id', 'nickname'], order: 'nickname ASC' }"
auto-load
@on-fetch="(data) => (buyersOptions = data)"
/>
<QToolbar class="bg-vn-dark justify-end">
<div id="st-data">
<TableVisibleColumns
:all-columns="allColumnNames"
table-code="itemsIndex"
labels-traductions-path="item.list"
@on-config-saved="visibleColumns = ['picture', ...$event, 'actions']"
/>
</div>
<QSpace />
<div id="st-actions"></div>
</QToolbar>
<QPage class="column items-center q-pa-md">
<VnPaginate
ref="paginateRef"
data-key="ItemList"
url="Items/filter"
:order="['isActive DESC', 'name', 'id']"
:limit="12"
:expr-builder="exprBuilder"
:user-params="params"
:offset="50"
auto-load
>
<template #body="{ rows }">
<QTable
:rows="rows"
:columns="columns"
row-key="id"
:pagination="{ rowsPerPage: 0 }"
class="full-width q-mt-md"
:visible-columns="visibleColumns"
:no-data-label="t('globals.noResults')"
@row-click="(_, row) => redirectToItemSummary(row.id)"
>
<template #top-row="{ cols }">
<QTr>
<QTd
v-for="(col, index) in cols"
:key="index"
style="max-width: 100px"
>
<component
:is="col.columnFilter.component"
v-if="col.columnFilter"
v-model="col.columnFilter.filterValue"
v-bind="col.columnFilter.attrs"
v-on="col.columnFilter.event(col)"
dense
/>
</QTd>
</QTr>
</template>
<template #body-cell-picture="{ row }">
<QTd>
<QImg
:src="`/api/Images/catalog/50x50/${row.id}/download?access_token=${token}`"
spinner-color="primary"
:ratio="1"
height="50px"
width="50px"
class="image"
/>
</QTd>
</template>
<template #body-cell-id="{ row }">
<QTd @click.stop>
<QBtn flat color="primary">
{{ row.id }}
</QBtn>
<ItemDescriptorProxy :id="row.id" />
</QTd>
</template>
<template #body-cell-userName="{ row }">
<QTd @click.stop>
<QBtn flat color="primary" dense>
{{ row.userName }}
</QBtn>
<WorkerDescriptorProxy :id="row.buyerFk" />
</QTd>
</template>
<template #body-cell-description="{ row }">
<QTd class="col">
<span>{{ row.name }} {{ row.subName }}</span>
<fetched-tags :item="row" :max-length="6" />
</QTd>
</template>
<template #body-cell-isActive="{ row }">
<QTd>
<QCheckbox :model-value="!!row.isActive" disable />
</QTd>
</template>
<template #body-cell-actions="{ row }">
<QTd>
<QIcon
@click.stop="
openConfirmationModal(
t(`All it's properties will be copied`),
t('Do you want to clone this item?'),
() => cloneItem(row.id)
)
"
class="q-ml-sm"
color="primary"
name="vn:clone"
size="sm"
>
<QTooltip>
{{ t('Clone') }}
</QTooltip>
</QIcon>
<QIcon
@click.stop="viewSummary(row.id, ItemSummary)"
class="q-ml-md"
color="primary"
name="preview"
size="sm"
>
<QTooltip class="text-no-wrap">
{{ t('Preview') }}
</QTooltip>
</QIcon>
</QTd>
</template>
</QTable>
</template>
</VnPaginate>
<QPageSticky :offset="[20, 20]">
<QBtn @click="redirectToItemCreate()" color="primary" fab icon="add" />
<QTooltip class="text-no-wrap">
{{ t('New item') }}
</QTooltip>
</QPageSticky>
</QPage>
</template>
<i18n>
es:
New item: Nuevo artículo
All it's properties will be copied: Todas sus propiedades serán copiadas
Do you want to clone this item?: ¿Desea clonar este artículo?
Clone: Clonar
Preview: Vista previa
</i18n>

View File

@ -61,7 +61,7 @@ const data = ref(useCardDescription());
const setData = (entity) => { const setData = (entity) => {
if (!entity) return; if (!entity) return;
data.value = useCardDescription(entity.client.name, entity.id); data.value = useCardDescription(entity.client.name, entity.id);
state.set('ClaimDescriptor', entity); state.set('OrderDescriptor', entity);
}; };
const getConfirmationValue = (isConfirmed) => { const getConfirmationValue = (isConfirmed) => {

View File

@ -197,6 +197,15 @@ const warehouseList = ref([]);
/> />
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem>
<QItemSection>
<QCheckbox
v-model="params.isOk"
:label="t('Served')"
toggle-indeterminate
/>
</QItemSection>
</QItem>
</template> </template>
</VnFilterPanel> </VnFilterPanel>
</template> </template>
@ -212,6 +221,7 @@ en:
workerFk: Worker workerFk: Worker
from: From from: From
to: To to: To
Served: Served
es: es:
params: params:
warehouseFk: Almacén warehouseFk: Almacén
@ -229,4 +239,5 @@ es:
Worker: Trabajador Worker: Trabajador
From: Desde From: Desde
To: Hasta To: Hasta
Served: Servida
</i18n> </i18n>

View File

@ -11,8 +11,8 @@ 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 RouteSearchbar from 'pages/Route/Card/RouteSearchbar.vue';
import {useStateStore} from "stores/useStateStore"; import { useStateStore } from 'stores/useStateStore';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
@ -26,6 +26,7 @@ const defaultInitialData = {
description: '', description: '',
vehicleFk: null, vehicleFk: null,
workerFk: null, workerFk: null,
isOk: false,
}; };
const workerList = ref([]); const workerList = ref([]);
@ -211,6 +212,7 @@ const onSave = (data, response) => {
size="sm" size="sm"
v-model="data.isOk" v-model="data.isOk"
:label="t('Is served')" :label="t('Is served')"
clearable
/> />
</div> </div>
</VnRow> </VnRow>

View File

@ -73,9 +73,9 @@ const ticketColumns = ref([
align: 'left', align: 'left',
}, },
{ {
name: 'warehouse', name: 'state',
label: t('route.summary.warehouse'), label: t('route.summary.state'),
field: (row) => row?.warehouseName, field: (row) => row?.ticketStateName,
sortable: false, sortable: false,
align: 'left', align: 'left',
}, },
@ -187,6 +187,15 @@ const ticketColumns = ref([
:label="t('route.summary.packages')" :label="t('route.summary.packages')"
:value="getTotalPackages(entity.tickets)" :value="getTotalPackages(entity.tickets)"
/> />
<QCheckbox
:label="
entity.route.isOk
? t('route.summary.closed')
: t('route.summary.open')
"
v-model="entity.route.isOk"
:disable="true"
/>
</QCard> </QCard>
<QCard class="vn-one"> <QCard class="vn-one">
<div class="header"> <div class="header">
@ -274,10 +283,14 @@ en:
city: City city: City
pc: PC pc: PC
client: Client client: Client
warehouse: Warehouse state: State
m3: m3:
packaging: Packaging packaging: Packaging
ticket: Ticket ticket: Ticket
closed: Closed
open: Open
yes: Yes
no: No
es: es:
route: route:
summary: summary:
@ -299,6 +312,10 @@ es:
city: Población city: Población
pc: CP pc: CP
client: Cliente client: Cliente
warehouse: Almacén state: Estado
packaging: Encajado packaging: Encajado
closed: Cerrada
open: Abierta
yes:
no: No
</i18n> </i18n>

View File

@ -95,6 +95,13 @@ const columns = computed(() => [
sortable: true, sortable: true,
align: 'left', align: 'left',
}, },
{
name: 'isServed',
label: t('Served'),
field: (row) => Boolean(row.isOk),
sortable: true,
align: 'left',
},
{ {
name: 'actions', name: 'actions',
label: '', label: '',
@ -265,7 +272,7 @@ const openTicketsDialog = (id) => {
auto-load auto-load
> >
<template #body="{ rows }"> <template #body="{ rows }">
<div class="q-pa-md"> <div class="q-pa-md route-table">
<QTable <QTable
v-model:selected="selectedRows" v-model:selected="selectedRows"
:columns="columns" :columns="columns"
@ -279,7 +286,7 @@ const openTicketsDialog = (id) => {
:no-data-label="t('globals.noResults')" :no-data-label="t('globals.noResults')"
> >
<template #body-cell-worker="{ row }"> <template #body-cell-worker="{ row }">
<QTd> <QTd class="table-input-cell">
<VnSelectFilter <VnSelectFilter
:label="t('Worker')" :label="t('Worker')"
v-model="row.workerFk" v-model="row.workerFk"
@ -312,7 +319,7 @@ const openTicketsDialog = (id) => {
</QTd> </QTd>
</template> </template>
<template #body-cell-agency="{ row }"> <template #body-cell-agency="{ row }">
<QTd> <QTd class="table-input-cell">
<VnSelectFilter <VnSelectFilter
:label="t('Agency')" :label="t('Agency')"
v-model="row.agencyModeFk" v-model="row.agencyModeFk"
@ -329,7 +336,7 @@ const openTicketsDialog = (id) => {
</QTd> </QTd>
</template> </template>
<template #body-cell-vehicle="{ row }"> <template #body-cell-vehicle="{ row }">
<QTd> <QTd class="table-input-cell">
<VnSelectFilter <VnSelectFilter
:label="t('Vehicle')" :label="t('Vehicle')"
v-model="row.vehicleFk" v-model="row.vehicleFk"
@ -397,6 +404,19 @@ const openTicketsDialog = (id) => {
/> />
</QTd> </QTd>
</template> </template>
<template #body-cell-isServed="props">
<QTd>
<QCheckbox v-model="props.value" disable>
<QTooltip>
{{
props.value
? t('Route is closed')
: t('Route is not served')
}}
</QTooltip>
</QCheckbox>
</QTd>
</template>
<template #body-cell-actions="props"> <template #body-cell-actions="props">
<QTd :props="props"> <QTd :props="props">
<div class="flex items-center no-wrap table-actions"> <div class="flex items-center no-wrap table-actions">
@ -455,7 +475,7 @@ const openTicketsDialog = (id) => {
<style lang="scss" scoped> <style lang="scss" scoped>
.table-input-cell { .table-input-cell {
min-width: 150px; max-width: 143px;
} }
.route-list { .route-list {
@ -466,6 +486,11 @@ const openTicketsDialog = (id) => {
.table-actions { .table-actions {
gap: 12px; gap: 12px;
} }
.lock-icon-cell {
text-align: center;
margin-left: -20%;
}
</style> </style>
<i18n> <i18n>
en: en:
@ -479,6 +504,7 @@ es:
Description: Descripción Description: Descripción
Hour started: Hora inicio Hour started: Hora inicio
Hour finished: Hora fin Hour finished: Hora fin
Served: Servida
newRoute: Nueva Ruta newRoute: Nueva Ruta
Clone Selected Routes: Clonar rutas seleccionadas Clone Selected Routes: Clonar rutas seleccionadas
Select the starting date: Seleccione la fecha de inicio Select the starting date: Seleccione la fecha de inicio
@ -490,4 +516,6 @@ es:
Add ticket: Añadir tickets Add ticket: Añadir tickets
Preview: Vista previa Preview: Vista previa
Summary: Resumen Summary: Resumen
Route is closed: La ruta está cerrada
Route is not served: La ruta no está servida
</i18n> </i18n>

View File

@ -1,10 +1,9 @@
<script setup> <script setup>
import { onMounted, ref, computed, onUpdated } from 'vue'; import { ref, computed, onUpdated } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
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 { useRole } from 'src/composables/useRole'; import { useRole } from 'src/composables/useRole';
import { dashIfEmpty } from 'src/filters'; import { dashIfEmpty } from 'src/filters';
import VnUserLink from 'src/components/ui/VnUserLink.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue';
@ -28,12 +27,6 @@ const entityId = computed(() => $props.id || route.params.id);
const summaryRef = ref(); const summaryRef = ref();
const supplier = ref(); const supplier = ref();
const supplierUrl = ref();
onMounted(async () => {
await roleState.fetch();
supplierUrl.value = (await getUrl('supplier/')) + entityId.value;
});
async function setData(data) { async function setData(data) {
if (data) { if (data) {
@ -44,6 +37,10 @@ async function setData(data) {
const isAdministrative = computed(() => { const isAdministrative = computed(() => {
return roleState.hasAny(['administrative']); return roleState.hasAny(['administrative']);
}); });
function getUrl(section) {
return isAdministrative.value && `#/supplier/${entityId.value}/${section}`;
}
</script> </script>
<template> <template>
@ -58,15 +55,10 @@ const isAdministrative = computed(() => {
<template #body> <template #body>
<QCard class="vn-one"> <QCard class="vn-one">
<router-link <VnTitle
v-if="isAdministrative" :url="getUrl('basic-data')"
class="header link" :text="t('globals.summary.basicData')"
:to="{ name: 'SupplierBasicData', params: { id: entityId } }" />
>
{{ t('globals.summary.basicData') }}
<QIcon name="open_in_new" />
</router-link>
<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" />
<VnLv :label="t('supplier.summary.responsible')"> <VnLv :label="t('supplier.summary.responsible')">
@ -94,15 +86,10 @@ const isAdministrative = computed(() => {
/> />
</QCard> </QCard>
<QCard class="vn-one"> <QCard class="vn-one">
<router-link <VnTitle
v-if="isAdministrative" :url="getUrl('billing-data')"
class="header link" :text="t('supplier.summary.billingData')"
:to="{ name: 'SupplierBillingData', params: { id: entityId } }" />
>
{{ t('supplier.summary.billingData') }}
<QIcon name="open_in_new" />
</router-link>
<span v-else> {{ t('supplier.summary.billingData') }}</span>
<VnLv <VnLv
:label="t('supplier.summary.payMethod')" :label="t('supplier.summary.payMethod')"
:value="supplier.payMethod?.name" :value="supplier.payMethod?.name"
@ -117,15 +104,10 @@ 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">
<router-link <VnTitle
v-if="isAdministrative" :url="getUrl('fiscal-data')"
class="header link" :text="t('supplier.summary.fiscalData')"
:to="{ name: 'SupplierFiscalData', params: { id: entityId } }" />
>
{{ t('supplier.summary.fiscalData') }}
<QIcon name="open_in_new" />
</router-link>
<span v-else> {{ t('supplier.summary.fiscalData') }}</span>
<VnLv <VnLv
:label="t('supplier.summary.sageTaxType')" :label="t('supplier.summary.sageTaxType')"
:value="supplier.sageTaxType?.vat" :value="supplier.sageTaxType?.vat"
@ -152,15 +134,10 @@ const isAdministrative = computed(() => {
/> />
</QCard> </QCard>
<QCard class="vn-one"> <QCard class="vn-one">
<router-link <VnTitle
v-if="isAdministrative" :url="getUrl('fiscal-data')"
class="header link" :text="t('supplier.summary.fiscalAddress')"
:to="{ name: 'SupplierFiscalData', params: { id: entityId } }" />
>
{{ t('supplier.summary.fiscalAddress') }}
<QIcon name="open_in_new" />
</router-link>
<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" />
<VnLv :label="t('supplier.summary.street')" :value="supplier.street" /> <VnLv :label="t('supplier.summary.street')" :value="supplier.street" />

View File

@ -4,7 +4,6 @@ import { useI18n } from 'vue-i18n';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue'; import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnInput from 'src/components/common/VnInput.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import VnInputDate from 'components/common/VnInputDate.vue'; import VnInputDate from 'components/common/VnInputDate.vue';
@ -63,7 +62,7 @@ const decrement = (paramsObj, key) => {
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnSelectFilter <VnSelectFilter
:label="t('params.agencyModeFk')" :label="t('travelFilter.filter.agencyModeFk')"
v-model="params.agencyModeFk" v-model="params.agencyModeFk"
@update:model-value="searchFn()" @update:model-value="searchFn()"
:options="agenciesOptions" :options="agenciesOptions"
@ -79,7 +78,7 @@ const decrement = (paramsObj, key) => {
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnSelectFilter <VnSelectFilter
:label="t('params.warehouseOutFk')" :label="t('travelFilter.filter.warehouseOutFk')"
v-model="params.warehouseOutFk" v-model="params.warehouseOutFk"
@update:model-value="searchFn()" @update:model-value="searchFn()"
:options="warehousesOptions" :options="warehousesOptions"
@ -95,7 +94,7 @@ const decrement = (paramsObj, key) => {
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnSelectFilter <VnSelectFilter
:label="t('params.warehouseInFk')" :label="t('travelFilter.filter.warehouseInFk')"
v-model="params.warehouseInFk" v-model="params.warehouseInFk"
@update:model-value="searchFn()" @update:model-value="searchFn()"
:options="warehousesOptions" :options="warehousesOptions"
@ -113,7 +112,7 @@ const decrement = (paramsObj, key) => {
<QInput <QInput
v-model="params.scopeDays" v-model="params.scopeDays"
type="number" type="number"
:label="t('params.scopeDays')" :label="t('travelFilter.filter.scopeDays')"
dense dense
outlined outlined
rounded rounded
@ -211,10 +210,6 @@ const decrement = (paramsObj, key) => {
en: en:
params: params:
search: Id/Reference search: Id/Reference
agencyModeFk: Agency
warehouseInFk: Warehouse In
warehouseOutFk: Warehouse Out
scopeDays: Days onward
landedFrom: Landed from landedFrom: Landed from
landedTo: Landed to landedTo: Landed to
continent: Continent out continent: Continent out
@ -222,10 +217,6 @@ en:
es: es:
params: params:
search: Id/Referencia search: Id/Referencia
agencyModeFk: Agencia
warehouseInFk: Alm. entrada
warehouseOutFk: Alm. salida
scopeDays: Días adelante
landedFrom: Llegada desde landedFrom: Llegada desde
landedTo: Llegada hasta landedTo: Llegada hasta
continent: Cont. Salida continent: Cont. Salida

View File

@ -0,0 +1,6 @@
travelFilter:
filter:
warehouseOutFk: Warehouse Out
warehouseInFk: Warehouse In
agencyModeFk: Agency
scopeDays: Days onward

View File

@ -0,0 +1,6 @@
travelFilter:
filter:
warehouseInFk: Alm. entrada
warehouseOutFk: Alm. salida
agencyModeFk: Agencia
scopeDays: Días adelante

View File

@ -27,6 +27,7 @@ const workerFilter = {
}, },
{ relation: 'sip', scope: { fields: ['extension', 'secret'] } }, { relation: 'sip', scope: { fields: ['extension', 'secret'] } },
{ relation: 'department', scope: { include: { relation: 'department' } } }, { relation: 'department', scope: { include: { relation: 'department' } } },
{ relation: 'client', scope: {fields:['phone']} },
], ],
}; };
const workersFilter = { const workersFilter = {
@ -86,6 +87,7 @@ const maritalStatus = [
:label="t('Mobile extension')" :label="t('Mobile extension')"
clearable clearable
/> />
<VnInput v-model="data.client.phone" :label="t('Personal phone')" clearable />
</VnRow> </VnRow>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
@ -157,6 +159,7 @@ es:
Last name: Apellidos Last name: Apellidos
Business phone: Teléfono de empresa Business phone: Teléfono de empresa
Mobile extension: Extensión móvil Mobile extension: Extensión móvil
Personal phone: Teléfono personal
Boss: Jefe Boss: Jefe
Marital status: Estado civil Marital status: Estado civil
Married: Casado/a Married: Casado/a

View File

@ -0,0 +1,239 @@
<script setup>
import { ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import WorkerCalendarFilter from 'pages/Worker/Card/WorkerCalendarFilter.vue';
import FetchData from 'components/FetchData.vue';
import WorkerCalendarItem from 'pages/Worker/Card/WorkerCalendarItem.vue';
import { useStateStore } from 'stores/useStateStore';
import axios from 'axios';
const stateStore = useStateStore();
const route = useRoute();
const { t } = useI18n();
const workerCalendarFilterRef = ref(null);
const workerCalendarRef = ref(null);
const absenceType = ref(null);
const hasWorkCenter = ref(false);
const isSubordinate = ref(false);
const businessFk = ref(null);
const year = ref(Date.vnNew().getFullYear());
const contractHolidays = ref(null);
const yearHolidays = ref(null);
const eventsMap = ref({});
const festiveEventsMap = ref({});
const onFetchActiveContract = (data) => {
if (!data) return;
businessFk.value = data?.businessFk;
hasWorkCenter.value = Boolean(data?.workCenterFk);
};
const addEvent = (day, newEvent, isFestive = false) => {
const timestamp = new Date(day).getTime();
let event = eventsMap.value[timestamp];
if (!event) {
eventsMap.value[timestamp] = newEvent;
if (isFestive)
festiveEventsMap.value[timestamp] = JSON.parse(JSON.stringify(newEvent));
} else {
const oldName = event.name;
const oldEventWasFestive = event.isFestive;
Object.assign(event, newEvent);
event.isFestive = oldEventWasFestive;
event.name = `${oldName}, ${event.name}`;
}
};
const onFetchAbsences = (data) => {
if (!data) return;
eventsMap.value = {};
if (data.holidays) {
data.holidays.forEach((holiday) => {
const holidayDetail = holiday?.detail?.name;
const holidayType = holiday?.type?.name;
const holidayName = holidayDetail || holidayType;
addEvent(
holiday.dated,
{
name: holidayName,
isFestive: true,
},
true
);
});
}
if (data.absences) {
data.absences.forEach((absence) => {
let type = absence.absenceType;
addEvent(absence.dated, {
name: type.name,
color: type.rgb,
type: type.code,
absenceId: absence.id,
isFestive: false,
});
});
}
};
const getAbsences = async () => {
try {
const params = {
workerFk: route.params.id,
businessFk: businessFk.value,
year: year.value,
};
const { data } = await axios.get('Calendars/absences', { params });
if (data) onFetchAbsences(data);
return data;
} catch (error) {
console.error('Error fetching absences:', error);
return null;
}
};
const getHolidays = async (params) => {
try {
const { data } = await axios.get(`Workers/${route.params.id}/holidays`, {
params,
});
return data;
} catch (error) {
console.error('Error fetching holidays:', error);
return null;
}
};
const updateContractHolidays = async () => {
contractHolidays.value = await getHolidays({
businessFk: businessFk.value,
year: year.value,
});
};
const updateYearHolidays = async () => {
yearHolidays.value = await getHolidays({ year: year.value });
};
const refreshData = () => {
updateYearHolidays();
updateContractHolidays();
getAbsences();
};
const onDeletedEvent = (timestamp) => {
delete eventsMap.value[timestamp];
// Si el evento que eliminamos se encontraba dentro de un dia festivo, volvemos a agregar el evento festivo
if (festiveEventsMap.value[timestamp])
eventsMap.value[timestamp] = festiveEventsMap.value[timestamp];
};
watch([year, businessFk], () => refreshData());
</script>
<template>
<FetchData
:url="`Workers/${route.params.id}/activeContract`"
@on-fetch="onFetchActiveContract"
auto-load
/>
<FetchData
:url="`Workers/${route.params.id}/isSubordinate`"
@on-fetch="(data) => (isSubordinate = data)"
auto-load
/>
<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>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<QScrollArea class="fit text-grey-8">
<WorkerCalendarFilter
ref="workerCalendarFilterRef"
v-model:business-fk="businessFk"
v-model:year="year"
v-model:absence-type="absenceType"
:contract-holidays="contractHolidays"
:year-holidays="yearHolidays"
/>
</QScrollArea>
</QDrawer>
<QPage class="column items-center">
<QCard v-if="!hasWorkCenter">
<QCardSection class="text-center">
{{ t('Autonomous worker') }}
</QCardSection>
</QCard>
<QCard v-else class="full-width">
<QIcon
v-if="isSubordinate"
name="info"
size="sm"
class="absolute"
style="top: 14px; right: 14px"
>
<QTooltip max-width="250px">
{{ t('addAbsencesText') }}
</QTooltip>
</QIcon>
<div class="calendar-container">
<WorkerCalendarItem
ref="workerCalendarRef"
v-for="month in 12"
:key="month"
:year="year"
:month="month"
:absence-type="absenceType"
:business-fk="businessFk"
:events="eventsMap"
@refresh="refreshData"
@on-deleted-event="onDeletedEvent"
/>
</div>
</QCard>
</QPage>
</template>
<style lang="scss" scoped>
.calendar-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
gap: 32px;
padding: 40px;
justify-content: center;
}
</style>
<i18n>
en:
addAbsencesText: To start adding absences, click an absence type from the right menu and then on the day you want to add an absence
es:
Search worker: Buscar trabajador
You can search by worker id or name: Puedes buscar por id o nombre del trabajador
addAbsencesText: Para empezar a añadir ausencias, haz clic en un tipo de ausencia desde el menu de la derecha y después en el día que quieres añadir la ausencia
</i18n>

View File

@ -0,0 +1,248 @@
<script setup>
import WorkerEventLabel from 'pages/Worker/Card/WorkerEventLabel.vue';
import FetchData from 'components/FetchData.vue';
import { useI18n } from 'vue-i18n';
import VnSelectFilter from 'components/common/VnSelectFilter.vue';
import { useRoute } from 'vue-router';
import { computed, ref } from 'vue';
import { toDateFormat } from '../../../filters/date';
const { t } = useI18n();
const route = useRoute();
const props = defineProps({
businessFk: {
type: Number,
default: null,
},
year: {
type: [Number, String],
default: null,
},
absenceType: {
type: Object,
default: null,
},
contractHolidays: {
type: Object,
default: null,
},
yearHolidays: {
type: Object,
default: null,
},
});
const emit = defineEmits(['update:businessFk', 'update:year', 'update:absenceType']);
const selectedBusinessFk = computed({
get: () => props.businessFk,
set: (value) => emit('update:businessFk', value),
});
const selectedYear = computed({
get: () => props.year,
set: (value) => emit('update:year', value),
});
const selectedAbsenceType = computed({
get: () => props.absenceType,
set: (value) => {
if (value === props.absenceType) value = null;
emit('update:absenceType', value);
},
});
const generateYears = () => {
const now = Date.vnNew();
const maxYear = now.getFullYear() + 1;
return Array.from({ length: 5 }, (_, i) => String(maxYear - i)) || [];
};
const absenceTypeList = ref([]);
const contractList = ref([]);
const yearList = ref(generateYears());
</script>
<template>
<FetchData
url="AbsenceTypes"
@on-fetch="(data) => (absenceTypeList = data)"
auto-load
/>
<FetchData
:url="`Workers/${route.params.id}/contracts`"
:filter="{ fields: ['businessFk', 'started', 'ended'] }"
@on-fetch="(data) => (contractList = data)"
auto-load
/>
<div
v-if="contractHolidays"
class="q-pa-md q-mb-md q-ma-md color-vn-text"
style="border: 2px solid black"
>
<QCardSection horizontal>
<span class="text-weight-bold text-subtitle1 text-center full-width">
{{ t('Contract') }} #{{ selectedBusinessFk }}
</span>
</QCardSection>
<QCardSection class="column items-center" horizontal>
<span>
{{
t('usedDays', {
holidaysEnjoyed: contractHolidays.holidaysEnjoyed || 0,
totalHolidays: contractHolidays.totalHolidays || 0,
})
}}
</span>
</QCardSection>
<QCardSection class="column items-center" horizontal>
<span>
{{
t('spentHours', {
hoursEnjoyed: contractHolidays.hoursEnjoyed || 0,
totalHours: contractHolidays.totalHours || 0,
})
}}
</span>
</QCardSection>
<QCardSection class="column items-center" horizontal>
<span>
{{
t('paidHolidays', {
payedHolidays: contractHolidays.payedHolidays || 0,
})
}}
</span>
</QCardSection>
</div>
<div
v-if="yearHolidays"
class="q-pa-md q-mb-md q-ma-md color-vn-text"
style="border: 2px solid black"
>
<QCardSection horizontal>
<span class="text-weight-bold text-subtitle1 text-center full-width">
{{ t('Year') }} {{ selectedYear }}
</span>
</QCardSection>
<QCardSection class="column items-center" horizontal>
<span>
{{
t('usedDays', {
holidaysEnjoyed: yearHolidays.holidaysEnjoyed || 0,
totalHolidays: yearHolidays.totalHolidays || 0,
})
}}
</span>
</QCardSection>
<QCardSection class="column items-center" horizontal>
{{
t('spentHours', {
hoursEnjoyed: yearHolidays.hoursEnjoyed || 0,
totalHours: yearHolidays.totalHours || 0,
})
}}
</QCardSection>
</div>
<QList dense class="list q-gutter-y-sm q-my-lg">
<QItem>
<QItemSection>
<VnSelectFilter
:label="t('Year')"
v-model="selectedYear"
:options="yearList"
dense
outlined
rounded
use-input
:is-clearable="false"
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnSelectFilter
:label="t('Contract')"
v-model="selectedBusinessFk"
:options="contractList"
option-value="businessFk"
option-label="businessFk"
dense
outlined
rounded
use-input
:is-clearable="false"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel># {{ scope.opt?.businessFk }}</QItemLabel>
<QItemLabel caption>
{{ toDateFormat(scope.opt?.started) }} -
{{
scope.opt?.ended
? toDateFormat(scope.opt?.ended)
: 'Indef.'
}}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</QItemSection>
</QItem>
</QList>
<QList dense class="list q-gutter-y-xs q-my-md">
<QItem v-for="type in absenceTypeList" :key="type.id">
<WorkerEventLabel
:color="type.rgb"
:selected="selectedAbsenceType?.id === type.id"
@click="selectedAbsenceType = type"
>
{{ type.name }}
</WorkerEventLabel>
</QItem>
</QList>
<QSeparator />
<QList dense class="list q-my-md no-pointer-events">
<QItem>
<WorkerEventLabel avatar-class="worker-calendar-festive">
{{ t('Festive') }}
</WorkerEventLabel>
<WorkerEventLabel avatar-class="worker-calendar-today">
{{ t('Current day') }}
</WorkerEventLabel>
</QItem>
</QList>
</template>
<style lang="scss">
.worker-calendar-festive {
border: 2px solid $negative;
}
.worker-calendar-today {
border: 2px solid $info;
}
</style>
<i18n>
en:
spentHours: Spent {hoursEnjoyed} of {totalHours} hours
usedDays: Used {holidaysEnjoyed} of {totalHolidays} days
paidHolidays: Paid holidays {payedHolidays} days
es:
Paid holidays: Vacaciones pagadas
Year: Año
Contract: Contrato
Festive: Festivo
Current day: Día actual
spentHours: Utilizadas {hoursEnjoyed} de {totalHours} horas
usedDays: Utilizados {holidaysEnjoyed} de {totalHolidays} días
paidHolidays: Vacaciones pagadas {payedHolidays} días
</i18n>

View File

@ -0,0 +1,313 @@
<script setup>
import { onBeforeMount, ref, watch, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { date } from 'quasar';
import { useRoute } from 'vue-router';
import QCalendarMonthWrapper from 'src/components/ui/QCalendarMonthWrapper.vue';
import { QCalendarMonth } from '@quasar/quasar-ui-qcalendar/src/index.js';
import '@quasar/quasar-ui-qcalendar/src/QCalendarVariables.sass';
import { useWeekdayStore } from 'src/stores/useWeekdayStore';
import useNotify from 'src/composables/useNotify.js';
import axios from 'axios';
const props = defineProps({
year: {
type: Number,
required: true,
},
month: {
type: Number,
required: true,
},
absenceType: {
type: Object,
default: null,
},
businessFk: {
type: Number,
default: null,
},
events: {
type: Object,
default: null,
},
});
const emit = defineEmits(['refresh', 'onDeletedEvent']);
const route = useRoute();
const { t } = useI18n();
const { notify } = useNotify();
const { locale } = useI18n();
const calendarRef = ref(null);
const weekdayStore = useWeekdayStore();
const selectedDate = ref();
const calendarEventDates = [];
const today = ref(date.formatDate(Date.vnNew(), 'YYYY-MM-DD'));
const todayTimestamp = computed(() => {
const date = Date.vnNew();
date.setHours(0, 0, 0, 0);
return date.getTime();
});
const _year = computed(() => props.year);
const updateSelectedDate = (year) => {
const _date = new Date(year, props.month - 1, 1);
selectedDate.value = date.formatDate(_date, 'YYYY-MM-DD');
};
const createEvent = async (date) => {
try {
const params = {
dated: date,
absenceTypeId: props.absenceType.id,
businessFk: props.businessFk,
};
const { data } = await axios.post(
`Workers/${route.params.id}/createAbsence`,
params
);
if (data) emit('refresh');
} catch (error) {
console.error('error creating event:: ', error);
}
};
const editEvent = async (event) => {
try {
const absenceType = props.absenceType;
const params = {
absenceId: event.absenceId,
absenceTypeId: absenceType.id,
};
const { data } = await axios.patch(
`Workers/${route.params.id}/updateAbsence`,
params
);
if (data) emit('refresh');
} catch (error) {
console.error('error editing event:: ', error);
}
};
const deleteEvent = async (event, date) => {
const params = { absenceId: event.absenceId };
const { data } = await axios.delete(`Workers/${route.params.id}/deleteAbsence`, {
params,
});
if (data) emit('onDeletedEvent', date.getTime());
};
const handleDateSelected = (date) => {
if (!props.absenceType) {
notify(t('Choose an absence type from the right menu'), 'warning');
return;
}
const { year, month, day } = date.scope.timestamp;
const _date = new Date(year, month - 1, day);
const stamp = _date.getTime();
const event = props.events[stamp];
if (!event) createEvent(_date);
};
const handleEventSelected = (event, { year, month, day }) => {
if (!props.absenceType) {
notify(t('Choose an absence type from the right menu'), 'warning');
return;
}
const date = new Date(year, month - 1, day);
if (!event.absenceId) createEvent(date);
else if (event.type == props.absenceType.code) deleteEvent(event, date);
else editEvent(event);
};
const getEventByTimestamp = ({ year, month, day }) => {
const stamp = new Date(year, month - 1, day).getTime();
return props.events[stamp] || null;
};
const getEventAttrs = (timestamp) => {
const event = getEventByTimestamp(timestamp);
if (!event) return {};
const { name, color, isFestive } = event;
// Atributos a asignar a cada slot que representa un evento en el calendario
const attrs = {
title: name,
style: color ? `background-color: ${color};` : '',
label: timestamp.day,
};
if (isFestive) {
attrs.class = '--festive';
attrs.label = event.absenceId ? timestamp.day : '';
}
return attrs;
};
const isToday = (timestamp) => {
const { year, month, day } = timestamp;
return todayTimestamp.value === new Date(year, month - 1, day).getTime();
};
onBeforeMount(() => {
updateSelectedDate(_year.value);
});
watch(_year, (newValue) => {
updateSelectedDate(newValue);
});
</script>
<template>
<QCalendarMonthWrapper
style="height: 290px; width: 310px"
class="outline"
bordered
transparent-background
>
<template #header>
<span class="full-width text-center text-body1 q-py-sm">{{
weekdayStore.getLocaleMonths[$props.month - 1].locale
}}</span>
</template>
<template #calendar>
<QCalendarMonth
ref="calendarRef"
v-model="selectedDate"
@click-date="handleDateSelected"
show-work-weeks
no-outside-days
:selected-dates="calendarEventDates"
no-active-date
:weekdays="[1, 2, 3, 4, 5, 6, 0]"
short-weekday-label
:locale="locale"
:now="today"
mini-mode
>
<template #day="{ scope: { timestamp } }">
<!-- Este slot representa cada día del calendario y muestra un botón representando el correspondiente evento -->
<QBtn
v-if="getEventByTimestamp(timestamp)"
v-bind="{ ...getEventAttrs(timestamp) }"
@click="
handleEventSelected(getEventByTimestamp(timestamp), timestamp)
"
rounded
dense
flat
class="calendar-event"
:class="{
'--today': isToday(timestamp),
}"
/>
</template>
</QCalendarMonth>
</template>
</QCalendarMonthWrapper>
</template>
<style lang="scss">
@import '../../../css/quasar.variables.scss';
:root {
// Cambia los colores del día actual del calendario por los de salix
--calendar-border-current-dark: #84d0e2 2px solid;
--calendar-border-current: #84d0e2 2px solid;
--calendar-current-color-dark: #ec8916;
}
.q-calendar__button {
width: 32px;
height: 32px;
font-size: 13px;
&:hover {
background-color: var(--vn-accent-color);
cursor: pointer;
}
}
.q-calendar-month__week--days > div:nth-child(6),
.q-calendar-month__week--days > div:nth-child(7) {
// Cambia el color de los días sábado y domingo
color: #777777;
}
.q-calendar-month__week--wrapper {
margin-bottom: 4px;
}
.q-calendar-month__workweek {
height: 32px;
display: flex;
justify-content: center;
}
.q-calendar__button--bordered {
color: $info !important;
}
.q-calendar-month__day--content {
position: absolute;
top: 1;
left: 0;
display: flex;
justify-content: center;
align-items: center;
}
.q-outside .calendar-event {
display: none;
}
.calendar-event {
display: flex;
justify-content: center;
width: 32px;
height: 32px;
font-size: 13px;
line-height: 1.715em;
cursor: pointer;
color: white;
&.--today {
border: 2px solid $info;
}
&.--festive {
border: 2px solid $negative;
}
&:hover {
opacity: 0.8;
}
}
.q-calendar-month__workweek,
.q-calendar-month__head--workweek,
.q-calendar-month__head--weekday.q-calendar__center.q-calendar__ellipsis {
text-transform: capitalize;
color: #777;
font-weight: bold;
font-size: 0.8rem;
text-align: center;
}
</style>
<i18n>
es:
Choose an absence type from the right menu: Elige un tipo de ausencia desde el menú de la derecha
</i18n>

View File

@ -31,29 +31,7 @@ const entityId = computed(() => {
}); });
const worker = ref(); const worker = ref();
const filter = { const filter = { where: { id: route.params.id}};
include: [
{
relation: 'user',
scope: {
fields: ['email', 'name', 'nickname'],
},
},
{
relation: 'department',
scope: {
include: [
{
relation: 'department',
},
],
},
},
{
relation: 'sip',
},
],
};
const sip = ref(null); const sip = ref(null);
@ -82,7 +60,7 @@ const setData = (entity) => {
<CardDescriptor <CardDescriptor
module="Worker" module="Worker"
data-key="workerData" data-key="workerData"
:url="`Workers/${entityId}`" :url="`Workers/summary`"
:filter="filter" :filter="filter"
:title="data.title" :title="data.title"
:subtitle="data.subtitle" :subtitle="data.subtitle"

View File

@ -0,0 +1,15 @@
<script setup>
import VnDmsList from 'src/components/common/VnDmsList.vue';
import { useRoute } from 'vue-router';
const route = useRoute();
</script>
<template>
<VnDmsList
:model="`WorkerDms/${route.params.id}/filter`"
update-model="Workers"
delete-model="WorkerDms"
download-model="WorkerDms"
default-dms-code="hhrrData"
filter="worker"
/>
</template>

View File

@ -0,0 +1,33 @@
<script setup>
defineProps({
color: {
type: String,
default: null,
},
avatarClass: {
type: String,
default: null,
},
selected: {
type: Boolean,
default: undefined,
},
});
defineEmits(['update:selected']);
</script>
<template>
<QChip
class="text-white q-ma-none"
:selected="selected"
:style="{ backgroundColor: selected ? color : 'black' }"
@update:selected="$emit('update:selected', $event)"
>
<QAvatar
:color="color"
:class="avatarClass"
:style="{ backgroundColor: color }"
/>
<slot />
</QChip>
</template>

View File

@ -10,7 +10,7 @@ import CardSummary from 'components/ui/CardSummary.vue';
import VnUserLink from 'src/components/ui/VnUserLink.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue';
import VnTitle from 'src/components/common/VnTitle.vue'; import VnTitle from 'src/components/common/VnTitle.vue';
const route = useRoute(); const { params } = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const $props = defineProps({ const $props = defineProps({
@ -20,53 +20,18 @@ const $props = defineProps({
}, },
}); });
const entityId = computed(() => $props.id || route.params.id); const entityId = computed(() => $props.id || params.id);
const workerUrl = ref(); const workerUrl = ref();
onMounted(async () => { onMounted(async () => {
workerUrl.value = (await getUrl('')) + `worker/${entityId.value}/`; workerUrl.value = (await getUrl('')) + `worker/${entityId.value}/`;
}); });
const filter = { const filter = { where: { id: entityId.value } };
include: [
{
relation: 'user',
scope: {
fields: ['email', 'name', 'nickname', 'roleFk'],
include: {
relation: 'role',
scope: {
fields: ['name'],
},
},
},
},
{
relation: 'department',
scope: {
include: {
relation: 'department',
scope: {
fields: ['name'],
},
},
},
},
{
relation: 'boss',
},
{
relation: 'client',
},
{
relation: 'sip',
},
],
};
</script> </script>
<template> <template>
<CardSummary ref="summary" :url="`Workers/${entityId}`" :filter="filter"> <CardSummary ref="summary" :url="`Workers/summary`" :filter="filter">
<template #header="{ entity }"> <template #header="{ entity }">
<div>{{ entity.id }} - {{ entity.firstName }} {{ entity.lastName }}</div> <div>{{ entity.id }} - {{ entity.firstName }} {{ entity.lastName }}</div>
</template> </template>
@ -103,12 +68,6 @@ const filter = {
<VnLinkPhone :phone-number="worker.phone" /> <VnLinkPhone :phone-number="worker.phone" />
</template> </template>
</VnLv> </VnLv>
<VnLv :value="worker.client?.phone">
<template #label>
{{ t('worker.summary.personalPhone') }}
<VnLinkPhone :phone-number="worker.client?.phone" />
</template>
</VnLv>
<VnLv :label="t('worker.summary.locker')" :value="worker.locker" /> <VnLv :label="t('worker.summary.locker')" :value="worker.locker" />
</QCard> </QCard>
<QCard class="vn-one"> <QCard class="vn-one">

View File

@ -45,7 +45,8 @@ export default {
path: 'create', path: 'create',
name: 'SupplierCreate', name: 'SupplierCreate',
meta: { meta: {
title: 'create', title: 'supplierCreate',
icon: 'add',
}, },
component: () => import('src/pages/Supplier/SupplierCreate.vue'), component: () => import('src/pages/Supplier/SupplierCreate.vue'),
}, },

View File

@ -55,7 +55,8 @@ export default {
path: 'create', path: 'create',
name: 'CustomerCreate', name: 'CustomerCreate',
meta: { meta: {
title: 'create', title: 'customerCreate',
icon: 'add',
}, },
component: () => import('src/pages/Customer/CustomerCreate.vue'), component: () => import('src/pages/Customer/CustomerCreate.vue'),
}, },

View File

@ -34,7 +34,8 @@ export default {
path: 'create', path: 'create',
name: 'EntryCreate', name: 'EntryCreate',
meta: { meta: {
title: 'create', title: 'entryCreate',
icon: 'add',
}, },
component: () => import('src/pages/Entry/EntryCreate.vue'), component: () => import('src/pages/Entry/EntryCreate.vue'),
}, },

View File

@ -10,15 +10,15 @@ export default {
component: RouterView, component: RouterView,
redirect: { name: 'ItemMain' }, redirect: { name: 'ItemMain' },
menus: { menus: {
main: [], main: ['ItemList', 'WasteBreakdown'],
card: [], card: ['ItemBasicData'],
}, },
children: [ children: [
{ {
path: '', path: '',
name: 'ItemMain', name: 'ItemMain',
component: () => import('src/pages/Item/ItemMain.vue'), component: () => import('src/pages/Item/ItemMain.vue'),
redirect: { name: 'Itemlist' }, redirect: { name: 'ItemList' },
children: [ children: [
{ {
path: 'list', path: 'list',
@ -29,6 +29,27 @@ export default {
}, },
component: () => import('src/pages/Item/ItemList.vue'), component: () => import('src/pages/Item/ItemList.vue'),
}, },
{
path: 'create',
name: 'ItemCreate',
meta: {
title: 'create',
},
component: () => import('src/pages/Item/ItemCreate.vue'),
},
{
path: 'waste-breakdown',
name: 'WasteBreakdown',
meta: {
title: 'wasteBreakdown',
icon: 'vn:claims',
},
beforeEnter: (to, from, next) => {
next({ name: 'ItemList' });
window.location.href =
'https://grafana.verdnatura.es/d/TTNXQAxVk';
},
},
], ],
}, },
{ {
@ -64,6 +85,15 @@ export default {
}, },
component: () => import('src/pages/Item/Card/ItemTags.vue'), component: () => import('src/pages/Item/Card/ItemTags.vue'),
}, },
{
path: 'basic-data',
name: 'ItemBasicData',
meta: {
title: 'basicData',
icon: 'vn:settings',
},
component: () => import('src/pages/Item/Card/ItemBasicData.vue'),
},
], ],
}, },
], ],

View File

@ -34,7 +34,8 @@ export default {
path: 'create', path: 'create',
name: 'OrderCreate', name: 'OrderCreate',
meta: { meta: {
title: 'create', title: 'orderCreate',
icon: 'add',
}, },
component: () => import('src/pages/Order/Card/OrderForm.vue'), component: () => import('src/pages/Order/Card/OrderForm.vue'),
}, },

View File

@ -34,7 +34,8 @@ export default {
path: 'create', path: 'create',
name: 'RouteCreate', name: 'RouteCreate',
meta: { meta: {
title: 'create', title: 'routeCreate',
icon: 'add',
}, },
component: () => import('src/pages/Route/Card/RouteForm.vue'), component: () => import('src/pages/Route/Card/RouteForm.vue'),
}, },

View File

@ -34,7 +34,8 @@ export default {
path: 'create', path: 'create',
name: 'ShelvingCreate', name: 'ShelvingCreate',
meta: { meta: {
title: 'create', title: 'shelvingCreate',
icon: 'add',
}, },
component: () => import('src/pages/Shelving/Card/ShelvingForm.vue'), component: () => import('src/pages/Shelving/Card/ShelvingForm.vue'),
}, },

View File

@ -44,6 +44,7 @@ export default {
name: 'TravelCreate', name: 'TravelCreate',
meta: { meta: {
title: 'travelCreate', title: 'travelCreate',
icon: 'add',
}, },
component: () => import('src/pages/Travel/TravelCreate.vue'), component: () => import('src/pages/Travel/TravelCreate.vue'),
}, },

View File

@ -19,6 +19,8 @@ export default {
'WorkerNotificationsManager', 'WorkerNotificationsManager',
'WorkerPBX', 'WorkerPBX',
'WorkerLog', 'WorkerLog',
'WorkerCalendar',
'WorkerDms',
], ],
departmentCard: ['BasicData'], departmentCard: ['BasicData'],
}, },
@ -52,7 +54,7 @@ export default {
name: 'WorkerCreate', name: 'WorkerCreate',
meta: { meta: {
title: 'workerCreate', title: 'workerCreate',
icon: '', icon: 'add',
}, },
component: () => import('src/pages/Worker/WorkerCreate.vue'), component: () => import('src/pages/Worker/WorkerCreate.vue'),
}, },
@ -127,6 +129,15 @@ export default {
}, },
component: () => import('src/pages/Worker/Card/WorkerPBX.vue'), component: () => import('src/pages/Worker/Card/WorkerPBX.vue'),
}, },
{
name: 'WorkerDms',
path: 'dms',
meta: {
title: 'dms',
icon: 'cloud_upload',
},
component: () => import('src/pages/Worker/Card/WorkerDms.vue'),
},
{ {
name: 'WorkerLog', name: 'WorkerLog',
path: 'log', path: 'log',
@ -136,6 +147,15 @@ export default {
}, },
component: () => import('src/pages/Worker/Card/WorkerLog.vue'), component: () => import('src/pages/Worker/Card/WorkerLog.vue'),
}, },
{
name: 'WorkerCalendar',
path: 'calendar',
meta: {
title: 'calendar',
icon: 'calendar_today',
},
component: () => import('src/pages/Worker/Card/WorkerCalendar.vue'),
},
], ],
}, },
], ],

View File

@ -7,6 +7,7 @@ import routes from 'src/router/modules';
export const useNavigationStore = defineStore('navigationStore', () => { export const useNavigationStore = defineStore('navigationStore', () => {
const modules = [ const modules = [
'item',
'shelving', 'shelving',
'order', 'order',
'customer', 'customer',

View File

@ -0,0 +1,95 @@
import { reactive, ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { defineStore } from 'pinia';
export const useWeekdayStore = defineStore('weekdayStore', () => {
const { t } = useI18n();
const weekdays = [
{ code: 'sun', name: 'Sunday' },
{ code: 'mon', name: 'Monday' },
{ code: 'tue', name: 'Tuesday' },
{ code: 'wed', name: 'Wednesday' },
{ code: 'thu', name: 'Thursday' },
{ code: 'fri', name: 'Friday' },
{ code: 'sat', name: 'Saturday' },
];
const monthCodes = [
'jan',
'feb',
'mar',
'apr',
'may',
'jun',
'jul',
'aug',
'sep',
'oct',
'nov',
'dec',
];
const localeOrder = {
es: ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'],
en: ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'],
};
const weekdaysMap = reactive({});
const localeWeekdays = ref([]);
const initStore = () => {
getWeekdaysMap();
};
const getWeekdaysMap = () => {
if (Object.keys(weekdaysMap).length > 0) return weekdaysMap;
weekdays.forEach((day, i) => {
const obj = {
...day,
index: i,
char: day.name.substr(0, 1),
abr: day.name.substr(0, 3),
};
weekdaysMap[day.code] = obj;
});
};
const getLocales = computed(() => {
// El día de mañana esto permitirá ordenar los weekdays en base a el locale si se lo desea reemplazando localeOrder.es por localeOrder[locale]
const locales = [];
for (let code of localeOrder.es) {
const obj = {
...weekdaysMap[code],
locale: t(`weekdays.${weekdaysMap[code].code}`),
localeChar: t(`weekdays.${weekdaysMap[code].code}`).substr(0, 1),
localeAbr: t(`weekdays.${weekdaysMap[code].code}`).substr(0, 3),
};
locales.push(obj);
}
return locales;
});
const getLocaleMonths = computed(() => {
const locales = [];
for (let code of monthCodes) {
const obj = {
code: code,
locale: t(`months.${code}`),
};
locales.push(obj);
}
return locales;
});
return {
initStore,
weekdaysMap,
localeWeekdays,
getLocales,
weekdays,
monthCodes,
getLocaleMonths,
};
});

View File

@ -1,48 +1,64 @@
const locationOptions ='[role="listbox"] > div.q-virtual-scroll__content > .q-item' const locationOptions = '[role="listbox"] > div.q-virtual-scroll__content > .q-item';
describe('VnLocation', () => { describe('VnLocation', () => {
describe('Create',()=>{ describe('Create', () => {
const inputLocation = ':nth-child(3) > :nth-child(1) > .q-field > .q-field__inner > .q-field__control'; const inputLocation =
'.q-form .q-card> :nth-child(3) > :nth-child(1) > .q-field > .q-field__inner > .q-field__control';
beforeEach(() => { beforeEach(() => {
cy.viewport(1280, 720); cy.viewport(1280, 720);
cy.login('developer'); cy.login('developer');
cy.visit('/#/worker/create'); cy.visit('/#/worker/create');
cy.waitForElement('.q-card'); cy.waitForElement('.q-card');
}); });
it('Show all options', function() { it('Show all options', function () {
cy.get(inputLocation).click(); cy.get(inputLocation).click();
cy.get(locationOptions).should('have.length.at.least',5); cy.get(locationOptions).should('have.length.at.least', 5);
}); });
it('input filter location as "al"', function() { it('input filter location as "al"', function () {
cy.get(inputLocation).click(); cy.get(inputLocation).click();
cy.get(inputLocation).clear(); cy.get(inputLocation).clear();
cy.get(inputLocation).type('al'); cy.get(inputLocation).type('al');
cy.get(locationOptions).should('have.length.at.least',3); cy.get(locationOptions).should('have.length.at.least', 3);
}); });
it('input filter location as "ecuador"', function() { it('input filter location as "ecuador"', function () {
cy.get(inputLocation).click(); cy.get(inputLocation).click();
cy.get(inputLocation).clear(); cy.get(inputLocation).clear();
cy.get(inputLocation).type('ecuador'); cy.get(inputLocation).type('ecuador');
cy.get(locationOptions).should('have.length.at.least',1); cy.get(locationOptions).should('have.length.at.least', 1);
cy.get(`${locationOptions}:nth-child(1)`).click(); cy.get(`${locationOptions}:nth-child(1)`).click();
cy.get(':nth-child(3) > :nth-child(1) > .q-field > .q-field__inner > .q-field__control > :nth-child(2) > .q-icon').click(); cy.get(inputLocation + '> :nth-child(2) > .q-icon').click();
}); });
}); });
describe('Fiscal-data',()=>{ describe('Fiscal-data', () => {
beforeEach(() => { beforeEach(() => {
cy.viewport(1280, 720); cy.viewport(1280, 720);
cy.login('developer'); cy.login('developer');
cy.visit('/#/supplier/567/fiscal-data', {timeout: 2000}); cy.visit('/#/supplier/567/fiscal-data', { timeout: 2000 });
cy.waitForElement('.q-card'); cy.waitForElement('.q-card');
}); });
it('Create postCode', function() { it('Create postCode', function () {
cy.get(':nth-child(6) > :nth-child(1) > .q-field > .q-field__inner > .q-field__control > :nth-child(3) > .q-icon').click(); cy.get(
':nth-child(6) > :nth-child(1) > .q-field > .q-field__inner > .q-field__control > :nth-child(3) > .q-icon'
).click();
cy.get(' .q-card > h1').should('have.text', 'New postcode'); cy.get(' .q-card > h1').should('have.text', 'New postcode');
cy.get('.q-card > :nth-child(4) > :nth-child(1) > .q-field > .q-field__inner > .q-field__control > :nth-child(1) > input').clear('12'); cy.get(
cy.get('.q-card > :nth-child(4) > :nth-child(1) > .q-field > .q-field__inner > .q-field__control > :nth-child(1) > input').type('1234453'); '.q-card > :nth-child(4) > :nth-child(1) > .q-field > .q-field__inner > .q-field__control > :nth-child(1) > input'
cy.selectOption('.q-dialog__inner > .column > #formModel > .q-card > :nth-child(4) > :nth-child(2) > .q-field > .q-field__inner > .q-field__control ', 'Valencia'); ).clear('12');
cy.selectOption('.q-dialog__inner > .column > #formModel > .q-card > :nth-child(5) > :nth-child(1) > .q-field > .q-field__inner > .q-field__control ', 'Province one'); cy.get(
cy.selectOption('.q-dialog__inner > .column > #formModel > .q-card > :nth-child(5) > :nth-child(2) > .q-field > .q-field__inner > .q-field__control ', 'España'); '.q-card > :nth-child(4) > :nth-child(1) > .q-field > .q-field__inner > .q-field__control > :nth-child(1) > input'
).type('1234453');
cy.selectOption(
'.q-dialog__inner > .column > #formModel > .q-card > :nth-child(4) > :nth-child(2) > .q-field > .q-field__inner > .q-field__control ',
'Valencia'
);
cy.selectOption(
'.q-dialog__inner > .column > #formModel > .q-card > :nth-child(5) > :nth-child(1) > .q-field > .q-field__inner > .q-field__control ',
'Province one'
);
cy.selectOption(
'.q-dialog__inner > .column > #formModel > .q-card > :nth-child(5) > :nth-child(2) > .q-field > .q-field__inner > .q-field__control ',
'España'
);
cy.get('.q-mt-lg > .q-btn--standard').click(); cy.get('.q-mt-lg > .q-btn--standard').click();
}); });
}); });
}) });

View File

@ -8,6 +8,7 @@ describe('ClaimDevelopment', () => {
cy.viewport(1920, 1080); cy.viewport(1920, 1080);
cy.login('developer'); cy.login('developer');
cy.visit(`/#/claim/${claimId}/development`); cy.visit(`/#/claim/${claimId}/development`);
cy.waitForElement('tbody');
}); });
it('should reset line', () => { it('should reset line', () => {

View File

@ -5,37 +5,40 @@ describe('WagonTypeCreate', () => {
cy.viewport(1920, 1080); cy.viewport(1920, 1080);
cy.login('developer'); cy.login('developer');
cy.visit(`/#/entry/${entryId}/dms`); cy.visit(`/#/entry/${entryId}/dms`);
}); });
it('should create edit and remove new dms', () => { it('should create edit and remove new dms', () => {
cy.addRow(); cy.addRow();
cy.get('.icon-attach').click() cy.get('.icon-attach').click();
cy.get('.q-file').selectFile('test/cypress/fixtures/image.jpg', { cy.get('.q-file').selectFile('test/cypress/fixtures/image.jpg', {
force: true, force: true,
}); });
cy.get("tbody > tr").then((value) => { cy.get('tbody > tr').then((value) => {
const u = undefined;
//Create and check if exist new row //Create and check if exist new row
let newFileTd = Cypress.$(value).length; let newFileTd = Cypress.$(value).length;
cy.get('.q-btn--standard > .q-btn__content > .block').click(); cy.get('.q-btn--standard > .q-btn__content > .block').click();
expect(value).to.have.length(newFileTd++); expect(value).to.have.length(newFileTd++);
const newRowSelector = `tbody > :nth-child(${newFileTd})` const newRowSelector = `tbody > :nth-child(${newFileTd})`;
cy.waitForElement(newRowSelector); cy.waitForElement(newRowSelector);
cy.validateRow(newRowSelector, [u, u, u, u, 'ENTRADA ID 1']);
//Edit new dms //Edit new dms
const u = undefined; const newDescription = 'entry id 1 modified';
cy.validateRow(newRowSelector, [u,u,u,u,'ENTRADA ID 1']) const textAreaSelector =
cy.get(`tbody :nth-child(${newFileTd}) > .text-right > .flex > :nth-child(2) > .q-btn > .q-btn__content > .q-icon`).click(); '.q-textarea > .q-field__inner > .q-field__control > .q-field__control-container';
}) cy.get(
// cy.log('newFileTd', newFileTd) `tbody :nth-child(${newFileTd}) > .text-right > .no-wrap > :nth-child(2) > .q-btn > .q-btn__content > .q-icon`
).click();
// //Create and check if exist new row cy.get(textAreaSelector).clear();
// cy.log('newFileTd:', newFileTd); cy.get(textAreaSelector).type(newDescription);
// cy.get(`tbody :nth-child(${newFileTd}) > .text-right > .flex > :nth-child(2) > .q-btn > .q-btn__content > .q-icon`).click() cy.saveCard();
cy.reload();
// cy.get(`tbody :nth-child(${newFileTd}) > :nth-child(5) > .q-tr > :nth-child(1) > span`).then((value) => { cy.validateRow(newRowSelector, [u, u, u, u, newDescription]);
// cy.log(value) });
// });
}); });
}); });

View File

@ -1,6 +1,6 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
describe('InvoiceInBasicData', () => { describe('InvoiceInBasicData', () => {
const selects = ':nth-child(1) > :nth-child(1) > .q-field'; const selects = '.q-form .q-card>:nth-child(1) > :nth-child(1) > .q-field';
const appendBtns = 'label button'; const appendBtns = 'label button';
const dialogAppendBtns = '.q-dialog label button'; const dialogAppendBtns = '.q-dialog label button';
const dialogInputs = '.q-dialog input'; const dialogInputs = '.q-dialog input';

View File

@ -37,6 +37,7 @@ describe('InvoiceInVat', () => {
it('should throw an error if there are fields undefined', () => { it('should throw an error if there are fields undefined', () => {
cy.get(inputBtns).eq(0).click(); cy.get(inputBtns).eq(0).click();
cy.get(':nth-child(1) > .q-td.q-table--col-auto-width > .q-field > .q-field__inner > .q-field__control > :nth-child(2) > .default-icon').click();
cy.get(dialogBtns).eq(2).click(); cy.get(dialogBtns).eq(2).click();
cy.get('.q-notification__message').should('have.text', "The code can't be empty"); cy.get('.q-notification__message').should('have.text', "The code can't be empty");
}); });
@ -44,7 +45,7 @@ describe('InvoiceInVat', () => {
it('should correctly handle expense addition', () => { it('should correctly handle expense addition', () => {
cy.get(inputBtns).eq(0).click(); cy.get(inputBtns).eq(0).click();
cy.get(dialogInputs).eq(0).click(); cy.get(':nth-child(1) > .q-td.q-table--col-auto-width > .q-field > .q-field__inner > .q-field__control > :nth-child(2) > .default-icon').click();
cy.get(dialogInputs).eq(0).type(randomInt); cy.get(dialogInputs).eq(0).type(randomInt);
cy.get(dialogInputs).eq(1).click(); cy.get(dialogInputs).eq(1).click();
cy.get(dialogInputs).eq(1).type('This is a dummy expense'); cy.get(dialogInputs).eq(1).type('This is a dummy expense');

View File

@ -36,7 +36,7 @@ describe('VnSearchBar', () => {
const checkCardListAndUrl = (expectedLength) => { const checkCardListAndUrl = (expectedLength) => {
cy.get(cardList).then(($cardList) => { cy.get(cardList).then(($cardList) => {
expect($cardList.find('.q-card').length).to.equal(expectedLength); expect($cardList.find('.q-card').length).to.equal(expectedLength);
cy.url().then((currentUrl) => expect(currentUrl).to.equal(url)); cy.url().then((currentUrl) => expect(currentUrl).to.contain(url));
}); });
}; };
}); });

View File

@ -8,9 +8,9 @@ describe('WorkerList', () => {
}); });
it('should load workers', () => { it('should load workers', () => {
cy.get(workerFieldNames).eq(0).should('have.text', 'JessicaJones'); cy.get(workerFieldNames).eq(0).should('have.text', 'jessicajones');
cy.get(workerFieldNames).eq(1).should('have.text', 'BruceBanner'); cy.get(workerFieldNames).eq(1).should('have.text', 'brucebanner');
cy.get(workerFieldNames).eq(2).should('have.text', 'CharlesXavier'); cy.get(workerFieldNames).eq(2).should('have.text', 'charlesxavier');
}); });
it('should open the worker summary', () => { it('should open the worker summary', () => {

View File

@ -60,7 +60,6 @@ describe('WorkerNotificationsManager', () => {
it('should active a notification if you are their boss', () => { it('should active a notification if you are their boss', () => {
cy.login('salesBoss'); cy.login('salesBoss');
cy.visit(`/#/worker/${salesPersonId}/notifications`); cy.visit(`/#/worker/${salesPersonId}/notifications`);
cy.waitForElement(activeList);
cy.waitForElement(availableList); cy.waitForElement(availableList);
cy.get(activeList) cy.get(activeList)

View File

@ -6,7 +6,8 @@ describe('WorkerSummary', () => {
}); });
it('should load worker summary', () => { it('should load worker summary', () => {
cy.get('.summaryHeader > div').should('have.text', '19 - salesBoss salesBoss'); cy.waitForElement('.summaryHeader');
cy.get('.summaryHeader > div').should('have.text', '19 - salesboss salesboss');
cy.get(':nth-child(1) > :nth-child(2) > .value > span').should( cy.get(':nth-child(1) > :nth-child(2) > .value > span').should(
'have.text', 'have.text',
'salesBossNick' 'salesBossNick'

View File

@ -72,6 +72,7 @@ Cypress.Commands.add('getValue', (selector) => {
// Fill Inputs // Fill Inputs
Cypress.Commands.add('selectOption', (selector, option) => { Cypress.Commands.add('selectOption', (selector, option) => {
cy.waitForElement(selector);
cy.get(selector).find('.q-select__dropdown-icon').click(); cy.get(selector).find('.q-select__dropdown-icon').click();
cy.get('.q-menu .q-item').contains(option).click(); cy.get('.q-menu .q-item').contains(option).click();
}); });
@ -183,11 +184,11 @@ Cypress.Commands.add('closeSideMenu', (element) => {
Cypress.Commands.add('clearSearchbar', (element) => { Cypress.Commands.add('clearSearchbar', (element) => {
if (element) cy.waitForElement(element); if (element) cy.waitForElement(element);
cy.get('#searchbar > form > label > div:nth-child(1) input').clear(); cy.get('#searchbar > form > div:nth-child(1) > label > div:nth-child(1) input').clear();
}); });
Cypress.Commands.add('writeSearchbar', (value) => { Cypress.Commands.add('writeSearchbar', (value) => {
cy.get('#searchbar > form > label > div:nth-child(1) input').type(value); cy.get('#searchbar > form > div:nth-child(1) > label > div:nth-child(1) input').type(value);
}); });
Cypress.Commands.add('validateContent', (selector, expectedValue) => { Cypress.Commands.add('validateContent', (selector, expectedValue) => {
cy.get(selector).should('have.text', expectedValue); cy.get(selector).should('have.text', expectedValue);

View File

@ -0,0 +1,31 @@
import { describe, expect, it, beforeAll } from 'vitest';
import { axios } from 'app/test/vitest/helper';
import { useArrayData } from 'composables/useArrayData';
describe('useArrayData', () => {
let arrayData;
beforeAll(() => {
axios.get.mockResolvedValue({ data: [] });
arrayData = useArrayData('InvoiceIn', { url: 'invoice-in/list' });
Object.defineProperty(window.location, 'href', {
writable: true,
value: 'localhost:9000/invoice-in/list',
});
// Mock the window.history.pushState method within useArrayData
window.history.pushState = (data, title, url) => (window.location.href = url);
// Mock the URL constructor within useArrayData
global.URL = class URL {
constructor(url) {
this.hash = url.split('localhost:9000/')[1];
}
};
});
it('should add the params to the url', async () => {
arrayData.store.userParams = { supplierFk: 2 };
arrayData.updateStateParams();
expect(window.location.href).contain('params=%7B%22supplierFk%22%3A2%7D');
});
});

View File

@ -19,7 +19,7 @@ export default defineConfig({
plugins: [ plugins: [
vue({ vue({
template: { template: {
transformAssetUrls transformAssetUrls,
}, },
}), }),
quasar({ quasar({
@ -27,7 +27,10 @@ export default defineConfig({
sassVariables: 'src/quasar-variables.scss', sassVariables: 'src/quasar-variables.scss',
}), }),
VueI18nPlugin({ VueI18nPlugin({
include: path.resolve(__dirname, 'src/i18n/**'), include: [
path.resolve(__dirname, 'src/i18n/**'),
path.resolve(__dirname, 'src/pages/**/locale/**'),
],
}), }),
jsconfigPaths(), jsconfigPaths(),
], ],