diff --git a/src/components/FormModel.vue b/src/components/FormModel.vue index 060a18bcc..6f91035ff 100644 --- a/src/components/FormModel.vue +++ b/src/components/FormModel.vue @@ -227,6 +227,7 @@ watch(formUrl, async () => { defineExpose({ save, isLoading, + hasChanges, }); </script> <template> diff --git a/src/css/app.scss b/src/css/app.scss index cac161d05..b36036df4 100644 --- a/src/css/app.scss +++ b/src/css/app.scss @@ -82,6 +82,11 @@ select:-webkit-autofill { color: $white; } +.card-width { + max-width: 800px; + width: 100%; +} + .vn-card { background-color: var(--vn-section-color); color: var(--vn-text-color); diff --git a/src/i18n/en/index.js b/src/i18n/en/index.js index 685591c2d..9b6ec06e3 100644 --- a/src/i18n/en/index.js +++ b/src/i18n/en/index.js @@ -851,12 +851,14 @@ export default { pageTitles: { workers: 'Workers', list: 'List', - basicData: 'Basic data', summary: 'Summary', - notifications: 'Notifications', workerCreate: 'New worker', department: 'Department', + basicData: 'Basic data', + notes: 'Notes', pda: 'PDA', + notifications: 'Notifications', + pbx: 'Private Branch Exchange', log: 'Log', }, list: { diff --git a/src/i18n/es/index.js b/src/i18n/es/index.js index abcc2a1bd..31e571565 100644 --- a/src/i18n/es/index.js +++ b/src/i18n/es/index.js @@ -850,12 +850,14 @@ export default { pageTitles: { workers: 'Trabajadores', list: 'Listado', - basicData: 'Datos básicos', summary: 'Resumen', - notifications: 'Notificaciones', workerCreate: 'Nuevo trabajador', department: 'Departamentos', + basicData: 'Datos básicos', + notes: 'Notas', pda: 'PDA', + notifications: 'Notificaciones', + pbx: 'Centralita', log: 'Historial', }, list: { diff --git a/src/pages/Worker/Card/WorkerBasicData.vue b/src/pages/Worker/Card/WorkerBasicData.vue new file mode 100644 index 000000000..c59f4281d --- /dev/null +++ b/src/pages/Worker/Card/WorkerBasicData.vue @@ -0,0 +1,168 @@ +<script setup> +import { ref } from 'vue'; +import { useRoute } from 'vue-router'; +import { useI18n } from 'vue-i18n'; + +import FetchData from 'components/FetchData.vue'; +import FormModel from 'src/components/FormModel.vue'; +import VnRow from 'components/ui/VnRow.vue'; +import VnInput from 'src/components/common/VnInput.vue'; +import VnSelectFilter from 'src/components/common/VnSelectFilter.vue'; + +const route = useRoute(); +const { t } = useI18n(); + +const workersOptions = ref([]); +const countriesOptions = ref([]); +const educationLevelsOptions = ref([]); + +const workerFilter = { + include: [ + { + relation: 'user', + scope: { + fields: ['name', 'emailVerified'], + include: { relation: 'emailUser', scope: { fields: ['email'] } }, + }, + }, + { relation: 'sip', scope: { fields: ['extension', 'secret'] } }, + { relation: 'department', scope: { include: { relation: 'department' } } }, + ], +}; +const workersFilter = { + fields: ['id', 'nickname'], + order: 'nickname ASC', + limit: 30, +}; +const countriesFilter = { + fields: ['id', 'country', 'code'], + order: 'country ASC', + limit: 30, +}; +const educationLevelsFilter = { fields: ['id', 'name'], order: 'name ASC', limit: 30 }; + +const maritalStatus = [ + { code: 'M', name: t('Married') }, + { code: 'S', name: t('Single') }, +]; +</script> + +<template> + <FetchData + :filter="workersFilter" + @on-fetch="(data) => (workersOptions = data)" + auto-load + url="Workers/search" + /> + <FetchData + :filter="countriesFilter" + @on-fetch="(data) => (countriesOptions = data)" + auto-load + url="Countries" + /> + <FetchData + :filter="educationLevelsFilter" + @on-fetch="(data) => (educationLevelsOptions = data)" + auto-load + url="EducationLevels" + /> + + <FormModel + :filter="workerFilter" + :url="`Workers/${route.params.id}`" + auto-load + model="Worker" + > + <template #form="{ data }"> + <VnRow class="row q-gutter-md q-mb-md"> + <VnInput :label="t('Name')" clearable v-model="data.firstName" /> + <VnInput :label="t('Last name')" clearable v-model="data.lastName" /> + </VnRow> + + <VnRow class="row q-gutter-md q-mb-md"> + <VnInput v-model="data.phone" :label="t('Business phone')" clearable /> + <VnInput + v-model="data.mobileExtension" + :label="t('Mobile extension')" + clearable + /> + </VnRow> + + <VnRow class="row q-gutter-md q-mb-md"> + <VnSelectFilter + :label="t('Boss')" + :options="workersOptions" + hide-selected + option-label="nickname" + option-value="id" + v-model="data.bossFk" + > + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel>{{ scope.opt?.name }}</QItemLabel> + <QItemLabel caption> + {{ scope.opt?.nickname }}, + {{ scope.opt?.code }} + </QItemLabel> + </QItemSection> + </QItem> + </template> + </VnSelectFilter> + <VnSelectFilter + :label="t('Marital status')" + :options="maritalStatus" + hide-selected + option-label="name" + option-value="code" + v-model="data.maritalStatus" + /> + </VnRow> + + <VnRow class="row q-gutter-md q-mb-md"> + <VnSelectFilter + :label="t('Origin country')" + :options="countriesOptions" + hide-selected + option-label="country" + option-value="id" + v-model="data.originCountryFk" + /> + <VnSelectFilter + :label="t('Education level')" + :options="educationLevelsOptions" + hide-selected + option-label="name" + option-value="id" + v-model="data.educationLevelFk" + /> + </VnRow> + + <VnRow class="row q-gutter-md q-mb-md"> + <VnInput v-model="data.SSN" :label="t('SSN')" clearable /> + <VnInput + v-model="data.locker" + type="number" + :label="t('Locker')" + clearable + /> + </VnRow> + </template> + </FormModel> +</template> + +<i18n> +es: + Name: Nombre + Last name: Apellidos + Business phone: Teléfono de empresa + Mobile extension: Extensión móvil + Boss: Jefe + Marital status: Estado civil + Married: Casado/a + Single: Soltero/a + Origin country: País origen + Education level: Nivel educación + SSN: NSS + Locker: Taquilla +</i18n> diff --git a/src/pages/Worker/Card/WorkerDescriptor.vue b/src/pages/Worker/Card/WorkerDescriptor.vue index 5144b3bfa..a20ad5546 100644 --- a/src/pages/Worker/Card/WorkerDescriptor.vue +++ b/src/pages/Worker/Card/WorkerDescriptor.vue @@ -1,5 +1,5 @@ <script setup> -import { computed, ref } from 'vue'; +import { computed, ref, watch } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import { useSession } from 'src/composables/useSession'; @@ -7,6 +7,7 @@ import CardDescriptor from 'src/components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue'; import useCardDescription from 'src/composables/useCardDescription'; +import { useState } from 'src/composables/useState'; const $props = defineProps({ id: { @@ -23,6 +24,7 @@ const $props = defineProps({ const route = useRoute(); const { t } = useI18n(); const { getTokenMultimedia } = useSession(); +const state = useState(); const entityId = computed(() => { return $props.id || route.params.id; @@ -53,7 +55,18 @@ const filter = { ], }; -const sip = computed(() => worker.value?.sip && worker.value.sip.extension); +const sip = ref(null); + +watch( + () => [worker.value?.sip?.extension, state.get('extension')], + ([newWorkerSip, newStateExtension], [oldWorkerSip, oldStateExtension]) => { + if (newStateExtension !== oldStateExtension || sip.value === oldStateExtension) { + sip.value = newStateExtension; + } else if (newWorkerSip !== oldWorkerSip && sip.value !== newStateExtension) { + sip.value = newWorkerSip; + } + } +); function getWorkerAvatar() { const token = getTokenMultimedia(); diff --git a/src/pages/Worker/Card/WorkerNotes.vue b/src/pages/Worker/Card/WorkerNotes.vue new file mode 100644 index 000000000..6c74aefa0 --- /dev/null +++ b/src/pages/Worker/Card/WorkerNotes.vue @@ -0,0 +1,38 @@ +<script setup> +import { useRoute } from 'vue-router'; + +import VnNotes from 'src/components/ui/VnNotes.vue'; + +const route = useRoute(); + +const filter = { + order: 'created DESC', + where: { workerFk: route.params.id }, + include: { + relation: 'worker', + scope: { + fields: ['id', 'firstName', 'lastName'], + include: { + relation: 'user', + scope: { + fields: ['id', 'nickname'], + }, + }, + }, + }, +}; + +const body = { + workerFk: route.params.id, +}; +</script> + +<template> + <VnNotes + style="overflow-y: auto" + :add-note="{ type: Boolean, default: true }" + url="WorkerObservations" + :filter="filter" + :body="body" + /> +</template> diff --git a/src/pages/Worker/Card/WorkerPBX.vue b/src/pages/Worker/Card/WorkerPBX.vue new file mode 100644 index 000000000..681194061 --- /dev/null +++ b/src/pages/Worker/Card/WorkerPBX.vue @@ -0,0 +1,70 @@ +<script setup> +import { watch, ref } from 'vue'; +import { useI18n } from 'vue-i18n'; +import { useRoute } from 'vue-router'; + +import { useState } from 'src/composables/useState'; + +import FormModel from 'src/components/FormModel.vue'; +import VnRow from 'components/ui/VnRow.vue'; +import VnInput from 'src/components/common/VnInput.vue'; + +const { t } = useI18n(); +const state = useState(); +const route = useRoute(); +const workerPBXForm = ref(); +const extension = ref(null); + +const filter = { + include: [ + { + relation: 'sip', + }, + ], +}; + +watch( + () => route.params.id, + () => state.set('extension', null) +); + +const onFetch = (data) => { + state.set('extension', data?.sip?.extension); + extension.value = state.get('extension'); +}; + +const updateModelValue = (data) => { + state.set('extension', data); + workerPBXForm.value.hasChanges = true; +}; +</script> + +<template> + <FormModel + ref="workerPBXForm" + :filter="filter" + :url="`Workers/${route.params.id}`" + url-update="Sips" + auto-load + :mapper=" + () => ({ + userFk: +route.params.id, + extension, + }) + " + model="DeviceProductionUser" + @on-fetch="onFetch" + > + <template #form="{}"> + <VnRow class="row q-gutter-md q-mb-md"> + <div class="col"> + <VnInput + :label="t('worker.summary.sipExtension')" + v-model="extension" + @update:model-value="updateModelValue" + /> + </div> + </VnRow> + </template> + </FormModel> +</template> diff --git a/src/router/modules/worker.js b/src/router/modules/worker.js index 448aab79c..7ea82d71d 100644 --- a/src/router/modules/worker.js +++ b/src/router/modules/worker.js @@ -12,7 +12,14 @@ export default { redirect: { name: 'WorkerMain' }, menus: { main: ['WorkerList', 'WorkerDepartment'], - card: ['WorkerNotificationsManager', 'WorkerPda', 'WorkerLog'], + card: [ + 'WorkerBasicData', + 'WorkerNotes', + 'WorkerPda', + 'WorkerNotificationsManager', + 'WorkerPBX', + 'WorkerLog', + ], departmentCard: ['BasicData'], }, children: [ @@ -66,6 +73,52 @@ export default { }, component: () => import('src/pages/Worker/Card/WorkerSummary.vue'), }, + { + path: 'basic-data', + name: 'WorkerBasicData', + meta: { + title: 'basicData', + icon: 'vn:settings', + }, + component: () => import('src/pages/Worker/Card/WorkerBasicData.vue'), + }, + { + path: 'notes', + name: 'NotesCard', + redirect: { name: 'WorkerNotes' }, + children: [ + { + path: '', + name: 'WorkerNotes', + meta: { + title: 'notes', + icon: 'vn:notes', + }, + component: () => + import('src/pages/Worker/Card/WorkerNotes.vue'), + }, + { + path: 'create', + name: 'WorkerNoteCreate', + meta: { + title: 'note-create', + }, + component: () => + import( + 'src/pages/Worker/components/WorkerNoteCreate.vue' + ), + }, + ], + }, + { + name: 'WorkerPda', + path: 'pda', + meta: { + title: 'pda', + icon: 'phone_android', + }, + component: () => import('src/pages/Worker/Card/WorkerPda.vue'), + }, { name: 'WorkerNotificationsManager', path: 'notifications', @@ -77,13 +130,13 @@ export default { import('src/pages/Worker/Card/WorkerNotificationsManager.vue'), }, { - name: 'WorkerPda', - path: 'pda', + path: 'pbx', + name: 'WorkerPBX', meta: { - title: 'pda', - icon: 'phone_android', + title: 'pbx', + icon: 'vn:pbx', }, - component: () => import('src/pages/Worker/Card/WorkerPda.vue'), + component: () => import('src/pages/Worker/Card/WorkerPBX.vue'), }, { name: 'WorkerLog',