WorkerCalendar #270
|
@ -32,6 +32,7 @@
|
|||
"@intlify/unplugin-vue-i18n": "^0.8.1",
|
||||
"@pinia/testing": "^0.1.2",
|
||||
"@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",
|
||||
"@vue/test-utils": "^2.4.4",
|
||||
"autoprefixer": "^10.4.14",
|
||||
|
|
|
@ -49,6 +49,9 @@ devDependencies:
|
|||
'@quasar/app-vite':
|
||||
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)
|
||||
'@quasar/quasar-app-extension-qcalendar':
|
||||
specifier: 4.0.0-beta.15
|
||||
version: 4.0.0-beta.15
|
||||
'@quasar/quasar-app-extension-testing-unit-vitest':
|
||||
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)
|
||||
|
@ -912,6 +915,13 @@ packages:
|
|||
resolution: {integrity: sha512-SlOhwzXyPQHWgQIS2ncyDdYdksCJvUYNtgsDQqzAKEG3r3d/ejOxvThle79HTK3Q6HB+gQWFG21Ux00Osr5XSw==}
|
||||
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):
|
||||
resolution: {integrity: sha512-eyzdUdmZiCueNS+5nedjMmzdbpCetSrtdGIwW6KplW1dTzRbLiNvYUjpBOxQGmJCgEhWy9zuswJ7MZ/bTql24Q==}
|
||||
engines: {node: '>= 12.22.1', npm: '>= 6.14.12', yarn: '>= 1.17.3'}
|
||||
|
@ -939,6 +949,10 @@ packages:
|
|||
- vite
|
||||
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:
|
||||
resolution: {integrity: sha512-A8RF99q6/sOSe1Ighnh5syEIbliD3qUYEJd2HyfFyBPSMF+WYGXon5dmzg4nUoK662NgOggInevkDyBDJcZugg==}
|
||||
engines: {node: '>= 16'}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
"@quasar/testing-unit-vitest": {
|
||||
"options": [
|
||||
"scripts"
|
||||
]
|
||||
}
|
||||
}
|
||||
"@quasar/testing-unit-vitest": {
|
||||
"options": ["scripts"]
|
||||
},
|
||||
"@quasar/qcalendar": {}
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -1,5 +1,6 @@
|
|||
// app global css in SCSS form
|
||||
@import './icons.scss';
|
||||
@import '@quasar/quasar-ui-qcalendar/src/QCalendarMonth.sass';
|
||||
|
||||
body.body--light {
|
||||
--font-color: black;
|
||||
|
@ -159,4 +160,15 @@ input::-webkit-inner-spin-button {
|
|||
appearance: none;
|
||||
-webkit-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;
|
||||
}
|
||||
|
|
|
@ -788,6 +788,7 @@ worker:
|
|||
dms: My documentation
|
||||
pbx: Private Branch Exchange
|
||||
log: Log
|
||||
calendar: Calendar
|
||||
list:
|
||||
name: Name
|
||||
email: Email
|
||||
|
@ -1148,3 +1149,24 @@ components:
|
|||
VnLv:
|
||||
copyText: '{copyValue} has been copied to the clipboard'
|
||||
iban_tooltip: 'IBAN: ES21 1234 5678 90 0123456789'
|
||||
weekdays:
|
||||
sun: Sunday
|
||||
mon: Monday
|
||||
tue: Tuesday
|
||||
wed: Wednesday
|
||||
thu: Thursday
|
||||
fri: Friday
|
||||
sat: Saturday
|
||||
months:
|
||||
jan: January
|
||||
feb: February
|
||||
mar: March
|
||||
apr: April
|
||||
may: May
|
||||
jun: June
|
||||
jul: July
|
||||
aug: August
|
||||
sep: September
|
||||
oct: October
|
||||
nov: November
|
||||
dec: December
|
||||
|
|
|
@ -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;
|
||||
jsegarra marked this conversation as resolved
Outdated
|
||||
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"
|
||||
jsegarra marked this conversation as resolved
jsegarra
commented
hace falta v-model? hace falta v-model?
jsegarra
commented
es que abajo no está :( es que abajo no está :(
wbuezas
commented
Los
De esta manera pasan la prop y handlean el evento de update. Los `v-models` en las props `businessFk`, `year` y `absenceType` son necesarios, porque utilizan estos emits:
`const emit = defineEmits(['update:businessFk', 'update:year', 'update:absenceType']);`
De esta manera pasan la prop y handlean el evento de update.
jsegarra
commented
Okey, ahora lo veo Okey, ahora lo veo
Gracias
|
||||
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>
|
||||
jsegarra marked this conversation as resolved
jsegarra
commented
Se que es un rollo, pero podemos mover este parrafo a traducciones como valor e inventar una clave que haga referencia, tipo: toStartAddingAbsences?? Se que es un rollo, pero podemos mover este parrafo a traducciones como valor e inventar una clave que haga referencia, tipo: toStartAddingAbsences??
wbuezas
commented
Claro, moví el texto a una clave de i18n. Commit: Claro, moví el texto a una clave de i18n.
Commit: https://gitea.verdnatura.es/verdnatura/salix-front/commit/e4166c85fabd899cd14c167149c9492bc1dc6bd7
|
||||
<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>
|
|
@ -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`"
|
||||
alexm
commented
Este if diria q no hace falta Este if diria q no hace falta
wbuezas
commented
Quitado Commit: Quitado
Commit: https://gitea.verdnatura.es/verdnatura/salix-front/commit/c12fd772184b8d7a26391c817d96a72004c3b392
|
||||
:filter="{ fields: ['businessFk', 'started', 'ended'] }"
|
||||
@on-fetch="(data) => (contractList = data)"
|
||||
auto-load
|
||||
/>
|
||||
alexm
commented
Y este where no debe estar. si no en el desplegable solo me muestra mi contrato actual Y este where no debe estar. si no en el desplegable solo me muestra mi contrato actual
wbuezas
commented
Quitado Commit: También me acabo de dar cuenta que estaba repetido el Commit: Quitado
Commit: https://gitea.verdnatura.es/verdnatura/salix-front/commit/c12fd772184b8d7a26391c817d96a72004c3b392
También me acabo de dar cuenta que estaba repetido el `businessFk` en `fields`
Commit: https://gitea.verdnatura.es/verdnatura/salix-front/commit/3d9d444bb425099ed13aeb709c85a7b87bbb3d38
|
||||
|
||||
<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,
|
||||
})
|
||||
}}
|
||||
jsegarra marked this conversation as resolved
Outdated
jsegarra
commented
la condicion if está bien? la condicion if está bien?
Porque en el bloque de antes se usaba contractHolidays y ahora yearHolidays
wbuezas
commented
Buena observación, cambie la variable Commit: Buena observación, cambie la variable `contractHolidays` por `yearHolidays`
Commit: https://gitea.verdnatura.es/verdnatura/salix-front/commit/196679b9e987719569eafcf57963510faf0b4121
|
||||
</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 }}
|
||||
jsegarra marked this conversation as resolved
Outdated
jsegarra
commented
Bien, pero si podemos tener traducción con parámetros quizás mejor Bien, pero si podemos tener traducción con parámetros quizás mejor
wbuezas
commented
Traducciones mejoradas. Commit: Traducciones mejoradas.
Commit: https://gitea.verdnatura.es/verdnatura/salix-front/commit/881c6e2f7fa214ffa04e461681ad1a4f9823b223
|
||||
</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,
|
||||
jsegarra marked this conversation as resolved
Outdated
jsegarra
commented
valor por defecto puede ir en la declaracion? valor por defecto puede ir en la declaracion?
wbuezas
commented
Valores por default movidos. Commit: Valores por default movidos.
Commit: https://gitea.verdnatura.es/verdnatura/salix-front/commit/0941d74d528fa910df2061f2f5782c83621ff8e9
|
||||
})
|
||||
}}
|
||||
</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>
|
|
@ -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';
|
||||
|
||||
jsegarra marked this conversation as resolved
Outdated
jsegarra
commented
Mmm...ya se esta usando en app.scss no? Mmm...ya se esta usando en app.scss no?
wbuezas
commented
Efectivamente, lo removí. Commit: Efectivamente, lo removí.
Commit: https://gitea.verdnatura.es/verdnatura/salix-front/commit/3e5a5dfe1c4a238a9ad6292b8bc6b54e50d13cfb
|
||||
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>
|
|
@ -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>
|
|
@ -19,6 +19,7 @@ export default {
|
|||
'WorkerNotificationsManager',
|
||||
'WorkerPBX',
|
||||
'WorkerLog',
|
||||
'WorkerCalendar',
|
||||
'WorkerDms',
|
||||
],
|
||||
departmentCard: ['BasicData'],
|
||||
|
@ -146,6 +147,15 @@ export default {
|
|||
},
|
||||
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'),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
});
|
Podemos usar chaining/? hoilday?.detail
Esa parte del código la había copiado exactamente igual a
salix
, aunque me parece bien la idea del chaining y lo apliqué.Commit:
b05c28f571