diff --git a/src/components/common/VnInputTime.vue b/src/components/common/VnInputTime.vue index 7ee93de19..0b4e72cb8 100644 --- a/src/components/common/VnInputTime.vue +++ b/src/components/common/VnInputTime.vue @@ -1,5 +1,5 @@ <script setup> -import { watch, computed, ref, nextTick } from 'vue'; +import { computed, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import { date } from 'quasar'; @@ -14,13 +14,13 @@ const props = defineProps({ default: false, }, }); +const initialDate = ref(model.value); const { t } = useI18n(); const requiredFieldRule = (val) => !!val || t('globals.fieldRequired'); const dateFormat = 'HH:mm'; const isPopupOpen = ref(); const hover = ref(); -const inputRef = ref(); const styleAttrs = computed(() => { return props.isOutlined @@ -50,7 +50,8 @@ const formattedTime = computed({ } if (!props.timeOnly) { const [hh, mm] = time.split(':'); - const date = new Date(model.value ? model.value : null); + + const date = new Date(model.value ? model.value : initialDate.value); date.setHours(hh, mm, 0); time = date?.toISOString(); } @@ -62,37 +63,10 @@ const formattedTime = computed({ function dateToTime(newDate) { return date.formatDate(new Date(newDate), dateFormat); } - -watch( - () => model.value, - (val) => (formattedTime.value = val), - { immediate: true } -); - -watch( - () => formattedTime.value, - async (val) => { - let position = 3; - const input = inputRef.value?.getNativeElement(); - if (!val || !input) return; - - let [hh, mm] = val.split(':'); - hh = parseInt(hh); - if (hh >= 10 || mm != '00') return; - - await nextTick(); - await nextTick(); - if (!hh) position = 0; - input.setSelectionRange(position, position); - }, - { immediate: true } -); </script> - <template> <div @mouseover="hover = true" @mouseleave="hover = false"> <QInput - ref="inputRef" class="vn-input-time" mask="##:##" placeholder="--:--" @@ -102,7 +76,7 @@ watch( style="min-width: 100px" :rules="$attrs.required ? [requiredFieldRule] : null" @click="isPopupOpen = false" - @focus="inputRef.getNativeElement().setSelectionRange(0, 0)" + type="time" > <template #append> <QIcon @@ -149,6 +123,11 @@ watch( border-style: solid; } </style> +<style lang="scss" scoped> +:deep(input[type='time']::-webkit-calendar-picker-indicator) { + display: none; +} +</style> <i18n> es: Open time: Abrir tiempo diff --git a/src/components/common/VnSelect.vue b/src/components/common/VnSelect.vue index 63a3f088f..b6dc226cc 100644 --- a/src/components/common/VnSelect.vue +++ b/src/components/common/VnSelect.vue @@ -73,6 +73,10 @@ const $props = defineProps({ type: Boolean, default: true, }, + params: { + type: Object, + default: null, + }, }); const { t } = useI18n(); @@ -153,9 +157,14 @@ async function fetchFilter(val) { ? optionValue.value : optionFilter.value ?? optionLabel.value); - const defaultWhere = $props.useLike - ? { [key]: { like: `%${val}%` } } - : { [key]: val }; + let defaultWhere = {}; + if ($props.filterOptions.length) { + defaultWhere = $props.filterOptions.reduce((obj, prop) => { + if (!obj.or) obj.or = []; + obj.or.push({ [prop]: getVal(val) }); + return obj; + }, {}); + } else defaultWhere = { [key]: getVal(val) }; const where = { ...(val ? defaultWhere : {}), ...$props.where }; const fetchOptions = { where, include, limit }; if (fields) fetchOptions.fields = fields; @@ -194,6 +203,8 @@ async function filterHandler(val, update) { function nullishToTrue(value) { return value ?? true; } + +const getVal = (val) => ($props.useLike ? { like: `%${val}%` } : val); </script> <template> @@ -205,6 +216,7 @@ function nullishToTrue(value) { :limit="limit" :sort-by="sortBy" :fields="fields" + :params="params" /> <QSelect v-model="value" diff --git a/src/components/ui/VnSearchbar.vue b/src/components/ui/VnSearchbar.vue index 2053efd62..a34eba6af 100644 --- a/src/components/ui/VnSearchbar.vue +++ b/src/components/ui/VnSearchbar.vue @@ -123,7 +123,7 @@ async function search() { <QForm @submit="search" id="searchbarForm"> <VnInput id="searchbar" - v-model="searchText" + v-model.trim="searchText" :placeholder="t(props.label)" dense standout diff --git a/src/pages/Account/Card/AccountDescriptor.vue b/src/pages/Account/Card/AccountDescriptor.vue index bc0c8c713..4571ee8ab 100644 --- a/src/pages/Account/Card/AccountDescriptor.vue +++ b/src/pages/Account/Card/AccountDescriptor.vue @@ -72,7 +72,7 @@ const hasAccount = ref(false); </VnImg> </template> <template #body="{ entity }"> - <VnLv :label="t('account.card.nickname')" :value="entity.nickname" /> + <VnLv :label="t('account.card.nickname')" :value="entity.name" /> <VnLv :label="t('account.card.role')" :value="entity.role.name" /> </template> <template #actions="{ entity }"> diff --git a/src/pages/Account/Card/AccountSummary.vue b/src/pages/Account/Card/AccountSummary.vue index 1901f9cde..5a21e18a5 100644 --- a/src/pages/Account/Card/AccountSummary.vue +++ b/src/pages/Account/Card/AccountSummary.vue @@ -48,7 +48,7 @@ const filter = { <QIcon name="open_in_new" /> </router-link> </QCardSection> - <VnLv :label="t('account.card.nickname')" :value="account.nickname" /> + <VnLv :label="t('account.card.nickname')" :value="account.name" /> <VnLv :label="t('account.card.role')" :value="account.role.name" /> </QCard> </template> diff --git a/src/pages/Ticket/Card/TicketTracking.vue b/src/pages/Ticket/Card/TicketTracking.vue index 2ddb278fa..53f59adf9 100644 --- a/src/pages/Ticket/Card/TicketTracking.vue +++ b/src/pages/Ticket/Card/TicketTracking.vue @@ -82,7 +82,9 @@ const openCreateModal = () => createTrackingDialogRef.value.show(); data-key="TicketTracking" :filter="paginateFilter" url="TicketTrackings" - auto-load + auto-load + order="created DESC" + :limit="0" > <template #body="{ rows }"> <QTable diff --git a/src/pages/Worker/Card/WorkerCalendarItem.vue b/src/pages/Worker/Card/WorkerCalendarItem.vue index 91e118708..05a013d6b 100644 --- a/src/pages/Worker/Card/WorkerCalendarItem.vue +++ b/src/pages/Worker/Card/WorkerCalendarItem.vue @@ -152,7 +152,7 @@ const getEventAttrs = (timestamp) => { if (isFestive) { attrs.class = '--festive'; - attrs.label = event.absenceId ?? timestamp.day; + attrs.label = timestamp.day; } else attrs.class = `--${type}`; return attrs; diff --git a/src/pages/Worker/Card/WorkerSummary.vue b/src/pages/Worker/Card/WorkerSummary.vue index 5c6261b94..33d949dbb 100644 --- a/src/pages/Worker/Card/WorkerSummary.vue +++ b/src/pages/Worker/Card/WorkerSummary.vue @@ -93,7 +93,6 @@ const filter = { /> </template> </VnLv> - <VnLv :label="t('worker.list.email')" :value="worker.user.email" copy /> <VnLv :label="t('worker.summary.boss')" link> <template #value> <VnUserLink @@ -139,29 +138,25 @@ const filter = { /> <VnLv :label="t('worker.summary.fi')" :value="worker.fi" /> <VnLv :label="t('worker.summary.birth')" :value="toDate(worker.birth)" /> - <VnRow class="q-mt-sm" wrap> - <VnLv - :label="t('worker.summary.isFreelance')" - :value="worker.isFreelance" - /> - <VnLv - :label="t('worker.summary.isSsDiscounted')" - :value="worker.isSsDiscounted" - /> - <VnLv - :label="t('worker.summary.hasMachineryAuthorized')" - :value="worker.hasMachineryAuthorized" - /> - <VnLv - :label="t('worker.summary.isDisable')" - :value="worker.isDisable" - /> - </VnRow> + <VnLv + :label="t('worker.summary.isFreelance')" + :value="worker.isFreelance" + /> + <VnLv + :label="t('worker.summary.isSsDiscounted')" + :value="worker.isSsDiscounted" + /> + <VnLv + :label="t('worker.summary.hasMachineryAuthorized')" + :value="worker.hasMachineryAuthorized" + /> + <VnLv :label="t('worker.summary.isDisable')" :value="worker.isDisable" /> </QCard> <QCard class="vn-one"> <VnTitle :text="t('worker.summary.userData')" /> <VnLv :label="t('worker.summary.userId')" :value="worker.user.id" /> <VnLv :label="t('worker.card.name')" :value="worker.user.nickname" /> + <VnLv :label="t('worker.list.email')" :value="worker.user.email" copy /> <VnLv :label="t('worker.summary.role')"> <template #value> <span class="link"> diff --git a/src/pages/Worker/Card/WorkerTimeControl.vue b/src/pages/Worker/Card/WorkerTimeControl.vue index 87ff44e63..f560fcfed 100644 --- a/src/pages/Worker/Card/WorkerTimeControl.vue +++ b/src/pages/Worker/Card/WorkerTimeControl.vue @@ -34,6 +34,10 @@ const weekdayStore = useWeekdayStore(); const weekDays = ref([]); const { openConfirmationModal } = useVnConfirm(); const { getWeekOfYear } = date; +const defaultDate = computed(() => { + const timestamp = route.query.timestamp; + return timestamp ? new Date(timestamp * 1000) : Date.vnNew(); +}); const workerTimeFormDialogRef = ref(null); const workerTimeReasonFormDialogRef = ref(null); @@ -56,7 +60,7 @@ const workerTimeFormProps = reactive({ // Array utilizado por QCalendar para seleccionar un rango de fechas const selectedCalendarDates = ref([]); // Date formateada para bindear al componente QDate -const selectedDateFormatted = ref(toDateString(Date.vnNew())); +const selectedDateFormatted = ref(toDateString(defaultDate.value)); const arrayData = useArrayData('workerData'); @@ -423,7 +427,7 @@ onBeforeMount(() => { }); onMounted(async () => { - await setDate(Date.vnNew()); + await setDate(defaultDate.value); await getMailStates(selectedDate.value); stateStore.rightDrawer = true; }); @@ -547,9 +551,12 @@ onMounted(async () => { <QTd v-for="(day, index) in props.cols" :key="index" - style="padding: 20px 16px !important" + :style="{ + padding: '20px 16px !important', + 'vertical-align': 'baseline', + }" > - <div class="full-height full-width column items-center"> + <div class="full-width column items-center"> <WorkerTimeHourChip v-for="(hour, ind) in day.dayData?.hours" :key="ind" diff --git a/src/pages/Worker/WorkerCreate.vue b/src/pages/Worker/WorkerCreate.vue index a1ded2bfd..297aa4182 100644 --- a/src/pages/Worker/WorkerCreate.vue +++ b/src/pages/Worker/WorkerCreate.vue @@ -2,7 +2,6 @@ import { onBeforeMount, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import axios from 'axios'; -import { useUserConfig } from 'src/composables/useUserConfig'; import VnRow from 'components/ui/VnRow.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import VnInputDate from 'components/common/VnInputDate.vue'; @@ -14,15 +13,25 @@ import FetchData from 'components/FetchData.vue'; import FormModel from 'components/FormModel.vue'; import CreateBankEntityForm from 'src/components/CreateBankEntityForm.vue'; import VnRadio from 'src/components/common/VnRadio.vue'; +import { useState } from 'src/composables/useState'; const { t } = useI18n(); +const user = useState().getUser(); const companiesOptions = ref([]); -const workersOptions = ref([]); const payMethodsOptions = ref([]); const bankEntitiesOptions = ref([]); -const formData = ref({ isFreelance: false }); -const defaultPayMethod = ref(0); +const formData = ref({ companyFk: user.value.companyFk, isFreelance: false }); +const defaultPayMethod = ref(); + +onBeforeMount(async () => { + defaultPayMethod.value = ( + await axios.get('WorkerConfigs/findOne', { + params: { field: ['payMethodFk'] }, + }) + ).data.payMethodFk; + formData.value.payMethodFk = defaultPayMethod.value; +}); function handleLocation(data, location) { const { town, code, provinceFk, countryFk } = location ?? {}; @@ -32,16 +41,32 @@ function handleLocation(data, location) { data.countryFk = countryFk; } -onBeforeMount(async () => { - const userInfo = await useUserConfig().fetch(); - formData.value.companyFk = userInfo.companyFk; +function generateCodeUser(worker) { + if (!worker.firstName || !worker.lastNames) return; - const { data } = await axios.get('WorkerConfigs/findOne', { - params: { field: ['payMethodFk'] }, - }); - defaultPayMethod.value = data.payMethodFk; - formData.value.payMethodFk = defaultPayMethod.value; -}); + const totalName = worker.firstName.concat(' ' + worker.lastNames).toLowerCase(); + const totalNameArray = totalName.split(' '); + let newCode = ''; + + for (let part of totalNameArray) newCode += part.charAt(0); + + worker.code = newCode.toUpperCase().slice(0, 3); + worker.name = totalNameArray[0] + newCode.slice(1); + + if (!worker.companyFk) worker.companyFk = user.companyFk; +} + +async function autofillBic(worker) { + if (!worker || !worker.iban) return; + + let bankEntityId = parseInt(worker.iban.substr(4, 4)); + let filter = { where: { id: bankEntityId } }; + + const { data } = await axios.get(`BankEntities`, { params: { filter } }); + const hasData = data && data[0]; + if (hasData) worker.bankEntityFk = data[0].id; + else if (!hasData) worker.bankEntityFk = undefined; +} </script> <template> <FetchData @@ -49,11 +74,6 @@ onBeforeMount(async () => { @on-fetch="(data) => (companiesOptions = data)" auto-load /> - <FetchData - url="Workers/search" - @on-fetch="(data) => (workersOptions = data)" - auto-load - /> <FetchData url="Paymethods" @on-fetch="(data) => (payMethodsOptions = data)" @@ -93,11 +113,13 @@ onBeforeMount(async () => { v-model="data.firstName" :label="t('worker.create.name')" :rules="validate('Worker.firstName')" + @update:model-value="generateCodeUser(data)" /> <VnInput v-model="data.lastNames" :label="t('worker.create.lastName')" :rules="validate('Worker.lastNames')" + @update:model-value="generateCodeUser(data)" /> <VnInput v-model="data.code" @@ -130,7 +152,7 @@ onBeforeMount(async () => { <VnSelect :label="t('worker.create.boss')" v-model="data.bossFk" - :options="workersOptions" + url="Workers/search" option-value="id" option-label="name" hide-selected @@ -204,6 +226,7 @@ onBeforeMount(async () => { :label="t('worker.create.iban')" :rules="validate('Worker.iban')" :disable="formData.isFreelance" + @update:model-value="autofillBic(data)" > <template #append> <QIcon name="info" class="cursor-info"> @@ -221,6 +244,8 @@ onBeforeMount(async () => { :roles-allowed-to-create="['salesAssistant', 'hr']" :rules="validate('Worker.bankEntity')" :disable="formData.isFreelance" + @update:model-value="autofillBic(data)" + :filter-options="['bic', 'name']" > <template #form> <CreateBankEntityForm diff --git a/src/pages/Worker/WorkerList.vue b/src/pages/Worker/WorkerList.vue index 7b3237f70..0ea094e23 100644 --- a/src/pages/Worker/WorkerList.vue +++ b/src/pages/Worker/WorkerList.vue @@ -1,5 +1,5 @@ <script setup> -import { computed, ref } from 'vue'; +import { onBeforeMount, computed, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; @@ -16,16 +16,19 @@ import CreateBankEntityForm from 'src/components/CreateBankEntityForm.vue'; import FetchData from 'src/components/FetchData.vue'; import RightMenu from 'src/components/common/RightMenu.vue'; import WorkerFilter from './WorkerFilter.vue'; +import { useState } from 'src/composables/useState'; +import axios from 'axios'; const { t } = useI18n(); const tableRef = ref(); const { viewSummary } = useSummaryDialog(); const companiesOptions = ref([]); -const workersOptions = ref([]); const payMethodsOptions = ref([]); const bankEntitiesOptions = ref([]); const postcodesOptions = ref([]); +const user = useState().getUser(); +const defaultPayMethod = ref(); const columns = computed(() => [ { align: 'left', @@ -82,6 +85,14 @@ const columns = computed(() => [ }, ]); +onBeforeMount(async () => { + defaultPayMethod.value = ( + await axios.get('WorkerConfigs/findOne', { + params: { field: ['payMethodFk'] }, + }) + ).data?.payMethodFk; +}); + function handleLocation(data, location) { const { town, code, provinceFk, countryFk } = location ?? {}; data.postcode = code; @@ -98,6 +109,31 @@ function uppercaseStreetModel(data) { }, }; } + +function generateCodeUser(worker) { + if (!worker.firstName || !worker.lastNames) return; + + const totalName = worker.firstName.concat(' ' + worker.lastNames).toLowerCase(); + const totalNameArray = totalName.split(' '); + let newCode = ''; + + for (let part of totalNameArray) newCode += part.charAt(0); + + worker.code = newCode.toUpperCase().slice(0, 3); + worker.name = totalNameArray[0] + newCode.slice(1); + + if (!worker.companyFk) worker.companyFk = user.companyFk; +} + +async function autofillBic(worker) { + if (!worker || !worker.iban) return; + + let bankEntityId = parseInt(worker.iban.substr(4, 4)); + let filter = { where: { id: bankEntityId } }; + + const { data } = await axios.get(`BankEntities`, { params: { filter } }); + worker.bankEntityFk = data?.[0]?.id ?? undefined; +} </script> <template> <VnSearchbar @@ -110,11 +146,6 @@ function uppercaseStreetModel(data) { @on-fetch="(data) => (companiesOptions = data)" auto-load /> - <FetchData - url="Workers/search" - @on-fetch="(data) => (workersOptions = data)" - auto-load - /> <FetchData url="Paymethods" @on-fetch="(data) => (payMethodsOptions = data)" @@ -131,6 +162,7 @@ function uppercaseStreetModel(data) { </template> </RightMenu> <VnTable + v-if="defaultPayMethod" ref="tableRef" data-key="Worker" url="Workers/filter" @@ -139,6 +171,8 @@ function uppercaseStreetModel(data) { title: t('Create worker'), onDataSaved: ({ id }) => tableRef.redirect(id), formInitialData: { + payMethodFk: defaultPayMethod, + companyFk: user.companyFk, isFreelance: false, }, }" @@ -149,7 +183,7 @@ function uppercaseStreetModel(data) { auto-load > <template #more-create-dialog="{ data }"> - <div class="q-pa-lg full-width" style="max-width: 1200px"> + <div class="q-pa-lg full-width"> <VnRadio v-model="data.isFreelance" :val="false" @@ -163,10 +197,16 @@ function uppercaseStreetModel(data) { @update:model-value="delete data.payMethodFk" /> <VnRow> - <VnInput v-model="data.firstName" :label="t('worker.create.name')" /> + <VnInput + next + v-model="data.firstName" + :label="t('worker.create.name')" + @update:model-value="generateCodeUser(data)" + /> <VnInput v-model="data.lastNames" :label="t('worker.create.lastName')" + @update:model-value="generateCodeUser(data)" /> <VnInput v-model="data.code" :label="t('worker.create.code')" /> </VnRow> @@ -189,7 +229,7 @@ function uppercaseStreetModel(data) { <VnSelect :label="t('worker.create.boss')" v-model="data.bossFk" - :options="workersOptions" + url="Workers/search" option-value="id" option-label="name" hide-selected @@ -254,6 +294,7 @@ function uppercaseStreetModel(data) { v-model="data.iban" :label="t('worker.create.iban')" :disable="data.isFreelance" + @update:model-value="autofillBic(data)" > <template #append> <QIcon name="info" class="cursor-info"> @@ -272,6 +313,8 @@ function uppercaseStreetModel(data) { hide-selected :roles-allowed-to-create="['salesAssistant', 'hr']" :disable="data.isFreelance" + @update:model-value="autofillBic(data)" + :filter-options="['bic', 'name']" > <template #form> <CreateBankEntityForm diff --git a/test/cypress/integration/route/routeList.spec.js b/test/cypress/integration/route/routeList.spec.js index afc0fc395..c9d7147c2 100644 --- a/test/cypress/integration/route/routeList.spec.js +++ b/test/cypress/integration/route/routeList.spec.js @@ -10,12 +10,13 @@ describe('Route', () => { it('Route list create route', () => { cy.get('.q-page-sticky > div > .q-btn > .q-btn__content > .q-icon').click(); - cy.get('input[name="description"]').eq(1).type('routeTestOne{enter}'); + cy.get('input[name="description"]').type('routeTestOne{enter}'); cy.get('.q-notification__message').should('have.text', 'Data created'); cy.url().should('include', '/summary'); }); it('Route list search and edit', () => { + cy.get('#searchbar input').type('{enter}'); cy.get('input[name="description"]').type('routeTestOne{enter}'); cy.get('.q-table tr') .its('length') diff --git a/test/cypress/integration/worker/workerCreate.spec.js b/test/cypress/integration/worker/workerCreate.spec.js index c1832ad67..50afe1892 100644 --- a/test/cypress/integration/worker/workerCreate.spec.js +++ b/test/cypress/integration/worker/workerCreate.spec.js @@ -2,6 +2,9 @@ describe('WorkerCreate', () => { const externalRadio = '.q-radio:nth-child(2)'; const notification = '.q-notification__message'; const developerBossId = 120; + const payMethodCross = + '.grid-create .full-width > :nth-child(9) .q-select .q-field__append:not(.q-anchor--skip)'; + const saveBtn = '.q-mt-lg > .q-btn--standard'; const internal = { Fi: { val: '78457139E' }, @@ -36,7 +39,8 @@ describe('WorkerCreate', () => { it('should throw an error if a pay method has not been selected', () => { cy.fillInForm(internal); - cy.get('.q-mt-lg > .q-btn--standard').click(); + cy.get(payMethodCross).click(); + cy.get(saveBtn).click(); cy.get(notification).should('contains.text', 'Payment method is required'); }); @@ -45,14 +49,14 @@ describe('WorkerCreate', () => { ...internal, 'Pay method': { val: 'PayMethod one', type: 'select' }, }); - cy.get('.q-mt-lg > .q-btn--standard').click(); + cy.get(saveBtn).click(); cy.get(notification).should('contains.text', 'Data created'); }); it('should create an external', () => { cy.get(externalRadio).click(); cy.fillInForm(external); - cy.get('.q-mt-lg > .q-btn--standard').click(); + cy.get(saveBtn).click(); cy.get(notification).should('contains.text', 'Data created'); }); });