closes #4834 create-worker-module #28

Merged
joan merged 27 commits from 4834-create-worker-module into dev 2023-03-08 09:54:59 +00:00
33 changed files with 1196 additions and 161 deletions

View File

@ -9,7 +9,7 @@
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js
const { configure } = require('quasar/wrappers');
const VueI18nPlugin = require('@intlify/unplugin-vue-i18n/vite')
const VueI18nPlugin = require('@intlify/unplugin-vue-i18n/vite');
const path = require('path');
module.exports = configure(function (/* ctx */) {
@ -140,6 +140,8 @@ module.exports = configure(function (/* ctx */) {
// Quasar plugins
plugins: ['Notify', 'Dialog'],
//all: 'auto',
//autoImportComponentCase: 'pascal',
},
// animations: 'all', // --- includes all animations

View File

@ -21,7 +21,14 @@ onMounted(() => stateStore.setMounted());
<template>
<q-header class="bg-dark" color="white" elevated>
<q-toolbar class="q-py-sm q-px-md">
<q-btn flat @click="stateStore.toggleLeftDrawer()" round dense icon="menu">
<q-btn
@click="stateStore.toggleLeftDrawer()"
icon="menu"
class="q-mr-sm"
round
dense
flat
>
<q-tooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</q-tooltip>

View File

@ -16,7 +16,7 @@ const props = defineProps({
module: {
type: String,
required: true,
}
},
});
const slots = useSlots();
@ -24,14 +24,18 @@ const { t } = useI18n();
onMounted(() => fetch());
const emit = defineEmits(['onFetch']);
const entity = ref();
async function fetch() {
const params = {};
if (props.filter) params.filter = props.filter;
if (props.filter) params.filter = JSON.stringify(props.filter);
const { data } = await axios.get(props.url, { params });
entity.value = data;
emit('onFetch', data);
}
watch(props, async () => {
@ -46,9 +50,9 @@ watch(props, async () => {
<div class="header bg-primary q-pa-sm">
<router-link :to="{ name: `${module}List` }">
<q-btn round flat dense size="md" icon="view_list" color="white">
<q-tooltip>{{
t('components.cardDescriptor.mainList')
}}</q-tooltip>
<q-tooltip>
{{ t('components.cardDescriptor.mainList') }}
</q-tooltip>
</q-btn>
</router-link>
<router-link
@ -80,8 +84,9 @@ watch(props, async () => {
</q-menu>
</q-btn>
</div>
<slot name="before" />
<div class="body q-py-sm">
<q-list>
<q-list dense>
<q-item-label header class="ellipsis text-h5" :lines="1">
<slot name="description" :entity="entity">
<span>
@ -98,6 +103,7 @@ watch(props, async () => {
</q-list>
<slot name="body" :entity="entity" />
</div>
<slot name="after" />
</template>
<!-- Skeleton -->
<skeleton-descriptor v-if="!entity" />

View File

@ -1,20 +0,0 @@
<script setup>
import { nextTick, ref } from 'vue';
const $props = defineProps({
to: {
type: String,
required: true,
},
});
const isHeaderMounted = ref(false);
nextTick(() => {
isHeaderMounted.value = document.querySelector($props.to) !== null;
});
</script>
<template>
<teleport v-if="isHeaderMounted" :to="$props.to">
<slot />
</teleport>
</template>

View File

@ -87,15 +87,29 @@ async function search() {
</q-form>
</template>
<style lang="scss" scoped>
@media screen and (max-width: $breakpoint-xs-max) {
.q-field {
width: 250px;
}
}
@media screen and (min-width: $breakpoint-xs-max) {
.q-field {
width: 400px;
}
}
.q-field {
transition: width 0.36s;
}
</style>
<style lang="scss">
.cursor-info {
cursor: help;
}
#searchbar .q-field {
min-width: 350px;
}
.body--light #searchbar {
.q-field--standout.q-field--highlighted .q-field__control {
background-color: $grey-7;

View File

@ -345,6 +345,47 @@ export default {
totalWithVat: 'Amount',
},
},
worker: {
pageTitles: {
workers: 'Workers',
list: 'List',
basicData: 'Basic data',
summary: 'Summary',
},
list: {
name: 'Name',
email: 'Email',
phone: 'Phone',
mobile: 'Mobile',
active: 'Active',
department: 'Department',
schedule: 'Schedule',
},
card: {
workerId: 'Worker ID',
name: 'Name',
email: 'Email',
phone: 'Phone',
mobile: 'Mobile',
active: 'Active',
warehouse: 'Warehouse',
agency: 'Agency',
salesPerson: 'Sales person',
},
summary: {
basicData: 'Basic data',
boss: 'Boss',
phoneExtension: 'Phone extension',
entPhone: 'Enterprise phone',
personalPhone: 'Personal phone',
noBoss: 'No boss',
userData: 'User data',
userId: 'User ID',
role: 'Role',
sipExtension: 'Extension',
},
imageNotFound: 'Image not found',
},
components: {
topbar: {},
userPanel: {

View File

@ -345,6 +345,47 @@ export default {
totalWithVat: 'Importe',
},
},
worker: {
pageTitles: {
workers: 'Trabajadores',
list: 'Listado',
basicData: 'Datos básicos',
summary: 'Resumen',
},
list: {
name: 'Nombre',
email: 'Email',
phone: 'Teléfono',
mobile: 'Móvil',
active: 'Activo',
department: 'Departamento',
schedule: 'Horario',
},
card: {
workerId: 'ID Trabajador',
name: 'Nombre',
email: 'Email',
phone: 'Teléfono',
mobile: 'Móvil',
active: 'Activo',
warehouse: 'Almacén',
agency: 'Empresa',
salesPerson: 'Comercial',
},
summary: {
basicData: 'Datos básicos',
boss: 'Jefe',
phoneExtension: 'Extensión de teléfono',
entPhone: 'Teléfono de empresa',
personalPhone: 'Teléfono personal',
noBoss: 'Sin jefe',
userData: 'Datos de usuario',
userId: 'ID del usuario',
role: 'Rol',
sipExtension: 'Extensión',
},
imageNotFound: 'No se ha encontrado la imagen',
},
components: {
topbar: {},
userPanel: {

View File

@ -3,20 +3,19 @@ import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore';
import ClaimDescriptor from './ClaimDescriptor.vue';
import LeftMenu from 'components/LeftMenu.vue';
import TeleportSlot from 'components/ui/TeleportSlot.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
const stateStore = useStateStore();
const { t } = useI18n();
</script>
<template>
<teleport-slot to="#searchbar">
<Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()">
<VnSearchbar
data-key="ClaimList"
:label="t('Search claim')"
:info="t('You can search by claim id or customer name')"
/>
</teleport-slot>
</Teleport>
<q-drawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<q-scroll-area class="fit">
<claim-descriptor />

View File

@ -3,7 +3,6 @@ import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { useSession } from 'src/composables/useSession';
import { useStateStore } from 'stores/useStateStore';
import TeleportSlot from 'components/ui/TeleportSlot.vue';
import Paginate from 'src/components/PaginateData.vue';
import ClaimLogFilter from './ClaimLogFilter.vue';
@ -114,7 +113,7 @@ function actionColor(action) {
</Paginate>
</q-timeline>
</div>
<TeleportSlot to="#actions-append">
<Teleport v-if="stateStore.isHeaderMounted()" to="#actions-append">
<div class="row q-gutter-x-sm">
<q-btn flat @click="stateStore.toggleRightDrawer()" round dense icon="menu">
<q-tooltip bottom anchor="bottom right">
@ -122,7 +121,7 @@ function actionColor(action) {
</q-tooltip>
</q-btn>
</div>
</TeleportSlot>
</Teleport>
<q-drawer v-model="stateStore.rightDrawer" show-if-above side="right" :width="300">
<q-scroll-area class="fit text-grey-8">
<ClaimLogFilter data-key="ClaimLogs" />

View File

@ -1,22 +1,20 @@
<script setup>
import { ref, computed } from 'vue';
import axios from 'axios';
import { ref, computed } from 'vue';
import { useQuasar } from 'quasar';
import VnConfirm from 'src/components/ui/VnConfirm.vue';
import TeleportSlot from 'src/components/ui/TeleportSlot.vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useSession } from 'src/composables/useSession';
import { useStateStore } from 'stores/useStateStore';
import { useSession } from 'composables/useSession';
import VnConfirm from 'components/ui/VnConfirm.vue';
import FetchData from 'components/FetchData.vue';
const router = useRouter();
const quasar = useQuasar();
const { t } = useI18n();
const stateStore = useStateStore();
const session = useSession();
const token = session.getToken();
const quasar = useQuasar();
const claimId = computed(() => router.currentRoute.value.params.id);
@ -239,7 +237,10 @@ function onDrag() {
</div>
</div>
<teleport-slot v-if="!quasar.platform.is.mobile" to="#actions-prepend">
<Teleport
v-if="stateStore.isHeaderMounted() && !quasar.platform.is.mobile"
to="#actions-prepend"
>
<div class="row q-gutter-x-sm">
<label for="fileInput">
<q-btn
@ -262,27 +263,34 @@ function onDrag() {
</label>
<q-separator vertical />
</div>
</teleport-slot>
</Teleport>
<teleport-slot to=".q-footer">
<q-tabs align="justify" inline-label narrow-indicator>
<q-tab
@click="inputFile.nativeEl.click()"
icon="add_circle"
:label="t('globals.add')"
>
<q-input
ref="inputFile"
type="file"
style="display: none"
multiple
v-model="files"
@update:model-value="create()"
/>
<q-tooltip bottom> {{ t('globals.add') }} </q-tooltip>
</q-tab>
</q-tabs>
</teleport-slot>
<q-page-sticky
v-if="quasar.platform.is.mobile"
position="bottom"
:offset="[0, 0]"
expand
>
<q-toolbar class="bg-primary text-white q-pa-none">
<q-tabs class="full-width" align="justify" inline-label narrow-indicator>
<q-tab
@click="inputFile.nativeEl.click()"
icon="add_circle"
:label="t('globals.add')"
>
<q-input
ref="inputFile"
type="file"
style="display: none"
multiple
v-model="files"
@update:model-value="create()"
/>
<q-tooltip bottom> {{ t('globals.add') }} </q-tooltip>
</q-tab>
</q-tabs>
</q-toolbar>
</q-page-sticky>
<!-- MULTIMEDIA DIALOG START-->
<q-dialog

View File

@ -5,9 +5,9 @@ import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import { useRoute } from 'vue-router';
import { useArrayData } from 'src/composables/useArrayData';
import { useStateStore } from 'stores/useStateStore';
import Paginate from 'src/components/PaginateData.vue';
import FetchData from 'components/FetchData.vue';
import TeleportSlot from 'components/ui/TeleportSlot.vue';
import VnConfirm from 'src/components/ui/VnConfirm.vue';
import { toDate } from 'src/filters';
@ -15,6 +15,7 @@ import { toDate } from 'src/filters';
const quasar = useQuasar();
const route = useRoute();
const { t } = useI18n();
const stateStore = useStateStore();
const arrayData = useArrayData('ClaimRma');
const claim = ref();
@ -44,6 +45,12 @@ async function onFetch(data) {
}
async function addRow() {
if (!claim.value.rma) {
return quasar.notify({
message: `This claim is not associated to any RMA`,
type: 'negative',
});
}
const formData = {
code: claim.value.rma,
};
@ -138,20 +145,31 @@ async function remove(id) {
</paginate>
</div>
</div>
<teleport-slot v-if="!quasar.platform.is.mobile" to="#actions-prepend">
<Teleport
v-if="stateStore.isHeaderMounted() && !quasar.platform.is.mobile"
to="#actions-prepend"
>
<div class="row q-gutter-x-sm">
<q-btn @click="addRow()" icon="add" color="primary" dense rounded>
<q-tooltip bottom> {{ t('globals.add') }} </q-tooltip>
</q-btn>
<q-separator vertical />
</div>
</teleport-slot>
</Teleport>
<teleport-slot to=".q-footer">
<q-tabs align="justify" inline-label narrow-indicator>
<q-tab @click="addRow()" icon="add_circle" :label="t('globals.add')" />
</q-tabs>
</teleport-slot>
<q-page-sticky
v-if="quasar.platform.is.mobile"
position="bottom"
:offset="[0, 0]"
expand
>
<q-toolbar class="bg-primary text-white q-pa-none">
<q-tabs class="full-width" align="justify" inline-label narrow-indicator>
<q-tab @click="addRow()" icon="add_circle" :label="t('globals.add')" />
</q-tabs>
</q-toolbar>
</q-page-sticky>
</template>
<style lang="scss" scoped>
@ -170,3 +188,8 @@ async function remove(id) {
z-index: 2998;
}
</style>
<i18n>
es:
This claim is not associated to any RMA: Esta reclamación no está asociada a ninguna ARM
</i18n>

View File

@ -3,12 +3,11 @@ import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
import { useStateStore } from 'stores/useStateStore';
import { toDate } from 'src/filters/index';
import Paginate from 'src/components/PaginateData.vue';
import { toDate } from 'filters/index';
import Paginate from 'components/PaginateData.vue';
import ClaimSummaryDialog from './Card/ClaimSummaryDialog.vue';
import CustomerDescriptorPopover from 'src/pages/Customer/Card/CustomerDescriptorPopover.vue';
import TeleportSlot from 'components/ui/TeleportSlot.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import CustomerDescriptorPopover from 'pages/Customer/Card/CustomerDescriptorPopover.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue';
import ClaimFilter from './ClaimFilter.vue';
const stateStore = useStateStore();
@ -37,22 +36,30 @@ function viewSummary(id) {
</script>
<template>
<teleport-slot to="#searchbar">
<VnSearchbar
data-key="ClaimList"
:label="t('Search claim')"
:info="t('You can search by claim id or customer name')"
/>
</teleport-slot>
<teleport-slot to="#actions-append">
<div class="row q-gutter-x-sm">
<q-btn flat @click="stateStore.toggleRightDrawer()" round dense icon="menu">
<q-tooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</q-tooltip>
</q-btn>
</div>
</teleport-slot>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<VnSearchbar
data-key="ClaimList"
:label="t('Search claim')"
:info="t('You can search by claim id or customer name')"
/>
</Teleport>
<Teleport to="#actions-append">
<div class="row q-gutter-x-sm">
<q-btn
flat
@click="stateStore.toggleRightDrawer()"
round
dense
icon="menu"
>
<q-tooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</q-tooltip>
</q-btn>
</div>
</Teleport>
</template>
<q-drawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<q-scroll-area class="fit text-grey-8">
<ClaimFilter data-key="ClaimList" />

View File

@ -3,20 +3,19 @@ import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore';
import CustomerDescriptor from './CustomerDescriptor.vue';
import LeftMenu from 'components/LeftMenu.vue';
import TeleportSlot from 'components/ui/TeleportSlot.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
const stateStore = useStateStore();
const { t } = useI18n();
</script>
<template>
<teleport-slot to="#searchbar">
<Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()">
<VnSearchbar
data-key="CustomerList"
:label="t('Search customer')"
:info="t('You can search by customer id or name')"
/>
</teleport-slot>
</Teleport>
<q-drawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<q-scroll-area class="fit">
<CustomerDescriptor />

View File

@ -4,6 +4,7 @@ import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { toCurrency } from 'src/filters';
import CardDescriptor from 'components/ui/CardDescriptor.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
const $props = defineProps({
id: {
@ -30,7 +31,10 @@ const entityId = computed(() => {
{{ t('customer.card.salesPerson') }}
</q-item-label>
<q-item-label class="col q-ma-none">
{{ entity.salesPersonUser.name }}
<span class="link">
{{ entity.salesPersonUser.name }}
<WorkerDescriptorProxy :id="entity.salesPersonFk" />
</span>
</q-item-label>
</q-item>
<q-item class="row">

View File

@ -5,7 +5,6 @@ import { useQuasar } from 'quasar';
import { useStateStore } from 'stores/useStateStore';
import Paginate from 'src/components/PaginateData.vue';
import CustomerSummaryDialog from './Card/CustomerSummaryDialog.vue';
import TeleportSlot from 'components/ui/TeleportSlot.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import CustomerFilter from './CustomerFilter.vue';
@ -29,22 +28,30 @@ function viewSummary(id) {
</script>
<template>
<teleport-slot to="#searchbar">
<VnSearchbar
data-key="CustomerList"
:label="t('Search customer')"
:info="t('You can search by customer id or name')"
/>
</teleport-slot>
<teleport-slot to="#actions-append">
<div class="row q-gutter-x-sm">
<q-btn flat @click="stateStore.toggleRightDrawer()" round dense icon="menu">
<q-tooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</q-tooltip>
</q-btn>
</div>
</teleport-slot>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<VnSearchbar
data-key="CustomerList"
:label="t('Search customer')"
:info="t('You can search by customer id or name')"
/>
</Teleport>
<Teleport to="#actions-append">
<div class="row q-gutter-x-sm">
<q-btn
flat
@click="stateStore.toggleRightDrawer()"
round
dense
icon="menu"
>
<q-tooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</q-tooltip>
</q-btn>
</div>
</Teleport>
</template>
<q-drawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<q-scroll-area class="fit text-grey-8">
<CustomerFilter data-key="CustomerList" />
@ -113,7 +120,7 @@ function viewSummary(id) {
<q-btn
flat
round
color="orange"
color="primary"
icon="arrow_circle_right"
@click="navigate(row.id)"
>

View File

@ -3,20 +3,19 @@ import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore';
import InvoiceOutDescriptor from './InvoiceOutDescriptor.vue';
import LeftMenu from 'components/LeftMenu.vue';
import TeleportSlot from 'components/ui/TeleportSlot.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue';
const stateStore = useStateStore();
const { t } = useI18n();
</script>
<template>
<teleport-slot to="#searchbar">
<Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()">
<VnSearchbar
data-key="InvoiceOutList"
:label="t('Search invoice')"
:info="t('You can search by invoice reference')"
/>
</teleport-slot>
</Teleport>
<q-drawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<q-scroll-area class="fit">
<InvoiceOutDescriptor />

View File

@ -7,7 +7,6 @@ import { useStateStore } from 'stores/useStateStore';
import Paginate from 'src/components/PaginateData.vue';
import InvoiceOutSummaryDialog from './Card/InvoiceOutSummaryDialog.vue';
import { toDate, toCurrency } from 'src/filters/index';
import TeleportSlot from 'components/ui/TeleportSlot.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import InvoiceOutFilter from './InvoiceOutFilter.vue';
@ -34,22 +33,30 @@ function viewSummary(id) {
</script>
<template>
<teleport-slot to="#searchbar">
<VnSearchbar
data-key="InvoiceOutList"
:label="t('Search invoice')"
:info="t('You can search by invoice reference')"
/>
</teleport-slot>
<teleport-slot to="#actions-append">
<div class="row q-gutter-x-sm">
<q-btn flat @click="stateStore.toggleRightDrawer()" round dense icon="menu">
<q-tooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</q-tooltip>
</q-btn>
</div>
</teleport-slot>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<VnSearchbar
data-key="InvoiceOutList"
:label="t('Search invoice')"
:info="t('You can search by invoice reference')"
/>
</Teleport>
<Teleport to="#actions-append">
<div class="row q-gutter-x-sm">
<q-btn
flat
@click="stateStore.toggleRightDrawer()"
round
dense
icon="menu"
>
<q-tooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</q-tooltip>
</q-btn>
</div>
</Teleport>
</template>
<q-drawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<q-scroll-area class="fit text-grey-8">
<InvoiceOutFilter data-key="InvoiceOutList" />

View File

@ -3,20 +3,19 @@ import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore';
import TicketDescriptor from './TicketDescriptor.vue';
import LeftMenu from 'components/LeftMenu.vue';
import TeleportSlot from 'components/ui/TeleportSlot.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
const stateStore = useStateStore();
const { t } = useI18n();
</script>
<template>
<teleport-slot to="#searchbar">
<Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()">
<VnSearchbar
data-key="TicketList"
:label="t('Search ticket')"
:info="t('You can search by ticket id or alias')"
/>
</teleport-slot>
</Teleport>
<q-drawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<q-scroll-area class="fit">
<TicketDescriptor />

View File

@ -7,8 +7,6 @@ import { useStateStore } from 'stores/useStateStore';
import Paginate from 'src/components/PaginateData.vue';
import { toDate, toCurrency } from 'src/filters/index';
import TicketSummaryDialog from './Card/TicketSummaryDialog.vue';
import TeleportSlot from 'components/ui/TeleportSlot.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import TicketFilter from './TicketFilter.vue';
@ -71,22 +69,30 @@ function viewSummary(id) {
</script>
<template>
<teleport-slot to="#searchbar">
<VnSearchbar
data-key="TicketList"
:label="t('Search ticket')"
:info="t('You can search by ticket id or alias')"
/>
</teleport-slot>
<teleport-slot to="#actions-append">
<div class="row q-gutter-x-sm">
<q-btn flat @click="stateStore.toggleRightDrawer()" round dense icon="menu">
<q-tooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</q-tooltip>
</q-btn>
</div>
</teleport-slot>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<VnSearchbar
data-key="TicketList"
:label="t('Search ticket')"
:info="t('You can search by ticket id or alias')"
/>
</Teleport>
<Teleport to="#actions-append">
<div class="row q-gutter-x-sm">
<q-btn
flat
@click="stateStore.toggleRightDrawer()"
round
dense
icon="menu"
>
<q-tooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</q-tooltip>
</q-btn>
</div>
</Teleport>
</template>
<q-drawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<q-scroll-area class="fit text-grey-8">
<TicketFilter data-key="TicketList" />

View File

@ -0,0 +1,37 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore';
import WorkerDescriptor from './WorkerDescriptor.vue';
import LeftMenu from 'components/LeftMenu.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
const stateStore = useStateStore();
const { t } = useI18n();
</script>
<template>
<Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()">
<VnSearchbar
data-key="WorkerList"
:label="t('Search worker')"
:info="t('You can search by worker id or name')"
/>
</Teleport>
<q-drawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<q-scroll-area class="fit">
<WorkerDescriptor />
<q-separator />
<left-menu source="card" />
</q-scroll-area>
</q-drawer>
<q-page-container>
<q-page class="q-pa-md">
<router-view></router-view>
</q-page>
</q-page-container>
</template>
<i18n>
es:
Search worker: Buscar trabajador
You can search by worker id or name: Puedes buscar por id o nombre del trabajador
</i18n>

View File

@ -0,0 +1,138 @@
<script setup>
import { computed, ref } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useSession } from 'src/composables/useSession';
import CardDescriptor from 'src/components/ui/CardDescriptor.vue';
const $props = defineProps({
id: {
type: Number,
required: false,
default: null,
},
});
const route = useRoute();
const { t } = useI18n();
const { getToken } = useSession();
const entityId = computed(() => {
return $props.id || route.params.id;
});
const worker = ref();
const filter = {
include: [
{
relation: 'user',
scope: {
fields: ['email', 'name', 'nickname'],
},
},
{
relation: 'department',
scope: {
include: [
{
relation: 'department',
},
],
},
},
{
relation: 'sip',
},
],
};
const sip = computed(() => worker.value.sip && worker.value.sip.extension);
function getWorkerAvatar() {
const token = getToken();
return `/api/Images/user/160x160/${route.params.id}/download?access_token=${token}`;
}
</script>
<template>
<card-descriptor
module="Worker"
:url="`Workers/${entityId}`"
:filter="filter"
@on-fetch="(data) => (worker = data)"
>
<template #before>
<q-img :src="getWorkerAvatar()" class="photo">
<template #error>
<div
class="absolute-full bg-grey-10 text-center q-pa-md flex flex-center"
>
<div>
<div class="text-grey-5" style="opacity: 0.4; font-size: 5vh">
<q-icon name="vn:claims" />
</div>
<div class="text-grey-5" style="opacity: 0.4">
{{ t('worker.imageNotFound') }}
</div>
</div>
</div>
</template>
</q-img>
</template>
<template #description="{ entity }">
<span>
{{ entity.user.nickname }}
<q-tooltip>{{ entity.user.nickname }}</q-tooltip>
</span>
</template>
<template #body="{ entity }">
<q-list>
<q-item>
<q-item-section>
<q-item-label caption> {{ t('worker.card.name') }} </q-item-label>
<q-item-label>{{ entity.user.nickname }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>
{{ t('worker.card.email') }}
</q-item-label>
<q-item-label>{{ entity.user.email }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>
{{ t('worker.list.department') }}
</q-item-label>
<q-item-label>
{{ entity.department.department.name }}
</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>
{{ t('worker.card.phone') }}
</q-item-label>
<q-item-label>{{ entity.phone }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption
>{{ t('worker.summary.sipExtension') }}
</q-item-label>
<q-item-label>{{ sip }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</template>
</card-descriptor>
</template>
<style lang="scss" scoped>
.photo {
height: 256px;
}
</style>

View File

@ -0,0 +1,16 @@
<script setup>
import WorkerDescriptor from './WorkerDescriptor.vue';
const $props = defineProps({
id: {
type: Number,
required: true,
},
});
</script>
<template>
<q-popup-proxy>
<WorkerDescriptor v-if="$props.id" :id="$props.id" />
</q-popup-proxy>
</template>

View File

@ -0,0 +1,292 @@
<script setup>
import axios from 'axios';
import { ref, onMounted, computed, onUpdated } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import SkeletonSummary from 'components/ui/SkeletonSummary.vue';
import WorkerDescriptorProxy from './WorkerDescriptorProxy.vue';
onMounted(() => fetch());
onUpdated(() => fetch());
const route = useRoute();
const { t } = useI18n();
const $props = defineProps({
id: {
type: Number,
default: 0,
},
});
const entityId = computed(() => $props.id || route.params.id);
const worker = ref(null);
const filter = {
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',
},
],
};
function fetch() {
const id = entityId.value;
axios.get(`/Workers/${id}`, { params: { filter } }).then((response) => {
worker.value = response.data;
});
}
function sipExtension() {
if (worker.value.sip) return worker.value.sip.extension;
return '-';
}
</script>
<template>
<div class="summary container">
<q-card>
<SkeletonSummary v-if="!worker" />
<template v-if="worker">
<div class="header bg-primary q-pa-sm q-mb-md">
{{ worker.id }} - {{ worker.firstName }} {{ worker.lastName }}
</div>
<div class="row q-pa-md q-col-gutter-md q-mb-md">
<div class="col">
<q-list>
<q-item-label header class="text-h6">
{{ t('worker.summary.basicData') }}
</q-item-label>
<q-item>
<q-item-section>
<q-item-label caption> ID </q-item-label>
<q-item-label>{{ worker.id }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption
>{{ t('worker.card.name') }}
</q-item-label>
<q-item-label>
{{ worker.user.nickname }}
</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption
>{{ t('worker.list.department') }}
</q-item-label>
<q-item-label>{{
worker.department.department.name
}}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption
>{{ t('worker.list.email') }}
</q-item-label>
<q-item-label>{{ worker.user.email }}</q-item-label>
</q-item-section>
</q-item>
<q-item
class="items-start cursor-pointer q-hoverable"
v-if="worker.boss"
>
<q-item-section>
<q-item-label caption>
{{ t('worker.summary.boss') }}
</q-item-label>
<q-item-label>
<span class="link">
{{ worker.boss.name }}
<WorkerDescriptorProxy :id="worker.bossFk" />
</span>
</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption
>{{ t('worker.summary.phoneExtension') }}
</q-item-label>
<q-item-label>
{{
worker.mobileExtension == ''
? worker.mobileExtension
: '-'
}}
</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption
>{{ t('worker.summary.entPhone') }}
</q-item-label>
<q-item-label>{{
worker.phone == '' ? worker.phone : '-'
}}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption
>{{ t('worker.summary.personalPhone') }}
</q-item-label>
<q-item-label>{{
worker.client.phone == ''
? worker.client.phone
: '-'
}}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
<div class="col">
<q-list>
<q-item-label header class="text-h6">
{{ t('worker.summary.userData') }}
</q-item-label>
<q-item>
<q-item-section>
<q-item-label caption>
{{ t('worker.summary.userId') }}
</q-item-label>
<q-item-label>{{ worker.user.id }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption
>{{ t('worker.card.name') }}
</q-item-label>
<q-item-label>{{
worker.user.nickname
}}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption
>{{ t('worker.summary.role') }}
</q-item-label>
<q-item-label>{{
worker.user.role.name
}}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption
>{{ t('worker.summary.sipExtension') }}
</q-item-label>
<q-item-label>{{ sipExtension() }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
</div>
</template>
</q-card>
</div>
</template>
<style lang="scss" scoped>
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 10px;
}
.container {
display: flex;
justify-content: center;
}
.q-card {
width: 100%;
max-width: 1200px;
}
.negative {
color: red;
}
.summary {
.q-list {
.q-item__label--header {
display: flex;
justify-content: space-between;
a {
color: $primary;
}
}
}
.row {
flex-wrap: wrap;
.col {
min-width: 250px;
}
}
.header {
text-align: center;
font-size: 18px;
}
#slider-container {
max-width: 80%;
margin: 0 auto;
.q-slider {
.q-slider__marker-labels:nth-child(1) {
transform: none;
}
.q-slider__marker-labels:nth-child(2) {
transform: none;
left: auto !important;
right: 0%;
}
}
}
}
.q-dialog .summary {
max-width: 1200px;
}
</style>

View File

@ -0,0 +1,21 @@
<script setup>
import { useDialogPluginComponent } from 'quasar';
import WorkerSummary from './WorkerSummary.vue';
const $props = defineProps({
id: {
type: Number,
required: true,
},
});
defineEmits([...useDialogPluginComponent.emits]);
const { dialogRef, onDialogHide } = useDialogPluginComponent();
</script>
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide">
<worker-summary v-if="$props.id" :id="$props.id" />
</q-dialog>
</template>

View File

@ -0,0 +1,121 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
const { t } = useI18n();
const props = defineProps({
dataKey: {
type: String,
required: true,
},
});
const departments = ref();
</script>
<template>
<fetch-data url="Departments" @on-fetch="(data) => (departments = data)" auto-load />
<VnFilterPanel :data-key="props.dataKey" :search-button="true">
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong>
<span>{{ formatFn(tag.value) }}</span>
</div>
</template>
<template #body="{ params, searchFn }">
<q-list dense>
<q-item>
<q-item-section>
<q-input :label="t('FI')" v-model="params.fi" lazy-rules>
<template #prepend>
<q-icon name="badge" size="sm"></q-icon>
</template>
</q-input>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-input
:label="t('First Name')"
v-model="params.firstName"
lazy-rules
/>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-input
:label="t('Last Name')"
v-model="params.lastName"
lazy-rules
/>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-input
:label="t('User Name')"
v-model="params.userName"
lazy-rules
/>
</q-item-section>
</q-item>
<q-item>
<q-item-section v-if="!departments">
<q-skeleton type="QInput" class="full-width" />
</q-item-section>
<q-item-section v-if="departments">
<q-select
:label="t('Department')"
v-model="params.departmentFk"
@update:model-value="searchFn()"
:options="departments"
option-value="id"
option-label="name"
emit-value
map-options
use-input
:input-debounce="0"
/>
</q-item-section>
</q-item>
<q-item class="q-mb-md">
<q-item-section>
<q-input
:label="t('Extension')"
v-model="params.extension"
lazy-rules
/>
</q-item-section>
</q-item>
</q-list>
</template>
</VnFilterPanel>
</template>
<i18n>
en:
params:
search: Contains
fi: FI
firstName: First name
lastName: Last name
userName: User
extension: Extension
es:
params:
search: Contiene
fi: NIF
firstName: Nombre
lastName: Apellidos
userName: Usuario
extension: Extensión
FI: NIF
First Name: Nombre
Last Name: Apellidos
User Name: Usuario
Department: Departamento
Extension: Extensión
</i18n>

View File

@ -0,0 +1,155 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
import { useStateStore } from 'stores/useStateStore';
import Paginate from 'src/components/PaginateData.vue';
import WorkerSummaryDialog from './Card/WorkerSummaryDialog.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import WorkerFilter from './WorkerFilter.vue';
const stateStore = useStateStore();
const router = useRouter();
const { t } = useI18n();
const quasar = useQuasar();
function navigate(id) {
router.push({ path: `/worker/${id}` });
}
function viewSummary(id) {
quasar.dialog({
component: WorkerSummaryDialog,
componentProps: {
id,
},
});
}
</script>
<template>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<VnSearchbar
data-key="WorkerList"
:label="t('Search worker')"
:info="t('You can search by worker id or name')"
/>
</Teleport>
<Teleport to="#actions-append">
<div class="row q-gutter-x-sm">
<q-btn
flat
@click="stateStore.toggleRightDrawer()"
round
dense
icon="menu"
>
<q-tooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</q-tooltip>
</q-btn>
</div>
</Teleport>
</template>
<q-drawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<q-scroll-area class="fit text-grey-8">
<WorkerFilter data-key="WorkerList" />
</q-scroll-area>
</q-drawer>
<q-page class="column items-center q-pa-md">
<div class="card-list">
<paginate
data-key="WorkerList"
url="Workers/filter"
order="id DESC"
auto-load
>
<template #body="{ rows }">
<q-card class="card q-mb-md" v-for="row of rows" :key="row.id">
<q-item
class="q-pa-none items-start cursor-pointer q-hoverable"
v-ripple
clickable
>
<q-item-section class="q-pa-md" @click="navigate(row.id)">
<q-item-label class="text-h6">
{{ row.nickname }}
</q-item-label>
<q-item-label caption>#{{ row.id }}</q-item-label>
<q-list>
<q-item class="q-pa-none">
<q-item-section>
<q-item-label caption>
{{ t('worker.list.name') }}
</q-item-label>
<q-item-label>{{
row.userName
}}</q-item-label>
</q-item-section>
</q-item>
<q-item class="q-pa-none">
<q-item-section>
<q-item-label caption>
{{ t('worker.list.email') }}
</q-item-label>
<q-item-label>{{ row.email }}</q-item-label>
</q-item-section>
</q-item>
<q-item class="q-pa-none">
<q-item-section>
<q-item-label caption>{{
t('worker.list.department')
}}</q-item-label>
<q-item-label>
{{ row.department }}
</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-item-section>
<q-separator vertical />
<q-card-actions vertical class="justify-between">
<q-btn
flat
round
color="primary"
icon="arrow_circle_right"
@click="navigate(row.id)"
>
<q-tooltip>
{{ t('components.smartCard.openCard') }}
</q-tooltip>
</q-btn>
<q-btn
flat
round
color="grey-7"
icon="preview"
@click="viewSummary(row.id)"
>
<q-tooltip>
{{ t('components.smartCard.openSummary') }}
</q-tooltip>
</q-btn>
</q-card-actions>
</q-item>
</q-card>
</template>
</paginate>
</div>
</q-page>
</template>
<style lang="scss" scoped>
.card-list {
width: 100%;
max-width: 60em;
}
</style>
<i18n>
es:
Search worker: Buscar trabajador
You can search by worker id or name: Puedes buscar por id o nombre del trabajador
</i18n>

View File

@ -0,0 +1,17 @@
<script setup>
import { useStateStore } from 'stores/useStateStore';
import LeftMenu from 'src/components/LeftMenu.vue';
const stateStore = useStateStore();
</script>
<template>
<q-drawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<q-scroll-area class="fit text-grey-8">
<LeftMenu />
</q-scroll-area>
</q-drawer>
<q-page-container>
<router-view></router-view>
</q-page-container>
</template>

View File

@ -2,10 +2,12 @@ import Customer from './customer';
import Ticket from './ticket';
import Claim from './claim';
import InvoiceOut from './invoiceOut';
import Worker from './worker';
export default [
Customer,
Ticket,
Claim,
InvoiceOut
InvoiceOut,
Worker
]

View File

@ -0,0 +1,51 @@
import { RouterView } from 'vue-router';
export default {
path: '/worker',
name: 'Worker',
meta: {
title: 'workers',
icon: 'vn:worker',
},
component: RouterView,
redirect: { name: 'WorkerMain' },
menus: {
main: ['WorkerList'],
card: [],
},
children: [
{
path: '',
name: 'WorkerMain',
component: () => import('src/pages/Worker/WorkerMain.vue'),
redirect: { name: 'WorkerList' },
children: [
{
path: 'list',
name: 'WorkerList',
meta: {
title: 'list',
icon: 'view_list',
},
component: () => import('src/pages/Worker/WorkerList.vue'),
},
],
},
{
name: 'WorkerCard',
path: ':id',
component: () => import('src/pages/Worker/Card/WorkerCard.vue'),
redirect: { name: 'WorkerSummary' },
children: [
{
name: 'WorkerSummary',
path: 'summary',
meta: {
title: 'summary',
},
component: () => import('src/pages/Worker/Card/WorkerSummary.vue'),
},
],
},
],
};

View File

@ -1,6 +1,7 @@
import customer from './modules/customer';
import ticket from './modules/ticket';
import claim from './modules/claim';
import worker from './modules/worker';
import invoiceOut from './modules/invoiceOut';
const routes = [
@ -26,6 +27,7 @@ const routes = [
customer,
ticket,
claim,
worker,
invoiceOut,
{
path: '/:catchAll(.*)*',

View File

@ -6,7 +6,7 @@ import { useRole } from 'src/composables/useRole';
import routes from 'src/router/modules';
export const useNavigationStore = defineStore('navigationStore', () => {
const modules = ['customer', 'claim', 'ticket', 'invoiceOut'];
const modules = ['customer', 'claim', 'ticket', 'invoiceOut', 'worker'];
const pinnedModules = ref([]);
const role = useRole();

View File

@ -0,0 +1,20 @@
describe('WorkerList', () => {
beforeEach(() => {
cy.viewport(1280, 720);
cy.login('developer');
cy.visit('/#/worker/list');
});
it('should load workers', () => {
cy.get('div[class="q-item__label text-h6"]').eq(0).should('have.text', 'Jessica Jones');
cy.get('div[class="q-item__label text-h6"]').eq(1).should('have.text', 'Bruce Banner');
cy.get('div[class="q-item__label text-h6"]').eq(2).should('have.text', 'Charles Xavier');
});
it('should open the worker summary', () => {
cy.get('div[class="q-item__section column q-item__section--side justify-center q-pa-md"]').eq(0).click();
cy.get('div[class="header bg-primary q-pa-sm q-mb-md"').should('have.text', '1110 - Jessica Jones');
cy.get('div[class="q-item__label q-item__label--header text-h6"]').eq(0).should('have.text', 'Basic data');
cy.get('div[class="q-item__label q-item__label--header text-h6"]').eq(1).should('have.text', 'User data');
});
});

View File

@ -0,0 +1,15 @@
describe('WorkerSummary', () => {
beforeEach(() => {
cy.viewport(1280, 720)
cy.login('developer')
cy.visit('/#/worker/19/summary');
});
it('should load worker summary', () => {
cy.get('div[class="header bg-primary q-pa-sm q-mb-md"').should('have.text', '19 - salesBoss');
cy.get('div[class="q-item__label q-item__label--header text-h6"]').eq(0).should('have.text', 'Basic data');
cy.get('div[class="q-item__label q-item__label--header text-h6"]').eq(1).should('have.text', 'User data');
cy.get('div[class="q-item__section column q-item__section--main justify-center"]').eq(0).should('have.text', 'NamesalesBossNick');
});
});