vacations (#5)

Reviewed-on: #5
This commit is contained in:
jose.lopez 2025-09-24 21:37:30 +00:00
parent 8079470f22
commit 68cde3dcea
5 changed files with 145 additions and 144 deletions

View File

@ -1,147 +1,150 @@
<script setup> <script setup>
import { computed } from 'vue'; import { computed, ref, onMounted } from 'vue';
import { api } from '@Services/Api';
import { apiTo } from './Module';
import GoogleIcon from '@Shared/GoogleIcon.vue'; import GoogleIcon from '@Shared/GoogleIcon.vue';
import VueApexCharts from 'vue3-apexcharts'; import VueApexCharts from 'vue3-apexcharts';
// Props // Propiedades
const props = defineProps({ const adminInfo = ref({});
adminInfo: { const vacationsCharts = ref({});
type: Object,
default: () => ({})
},
charts: {
type: Object,
default: () => ({})
}
});
// Datos procesados para los charts // Opciones para gráfica de solicitudes por mes (línea)
const chartData = computed(() => {
// Datos para el gráfico de línea (Solicitudes por Mes)
const monthlyData = props.charts.requests_by_month || [];
const lineChartCategories = monthlyData.map(item => item.month_name);
const lineChartSeries = [{
name: 'Solicitudes',
data: monthlyData.map(item => item.total_requests)
}];
// Datos para el gráfico de dona (Solicitudes por Departamento)
const departmentData = props.charts.requests_by_department || [];
const donutChartLabels = departmentData.map(item => item.department_name);
const donutChartSeries = departmentData.map(item => item.total_requests);
return {
lineChartCategories,
lineChartSeries,
donutChartLabels,
donutChartSeries
};
});
// Datos para el gráfico de línea (Solicitudes por Mes) - Solo para admin
const lineChartOptions = computed(() => ({ const lineChartOptions = computed(() => ({
chart: { chart: {
type: 'line', type: 'line',
height: 350, height: 350,
toolbar: { toolbar: {
show: false show: true
}, },
background: 'transparent' zoom: {
enabled: true
}
}, },
colors: ['#3b82f6'], colors: ['#3B82F6'],
stroke: { stroke: {
curve: 'smooth', curve: 'smooth',
width: 3 width: 3
}, },
grid: {
borderColor: 'rgba(148, 163, 184, 0.2)',
strokeDashArray: 3,
xaxis: {
lines: {
show: true
}
},
yaxis: {
lines: {
show: true
}
}
},
xaxis: {
categories: chartData.value.lineChartCategories,
labels: {
style: {
colors: '#64748b',
fontSize: '12px'
}
},
axisBorder: {
color: 'rgba(148, 163, 184, 0.2)'
}
},
yaxis: {
min: 0,
labels: {
style: {
colors: '#64748b',
fontSize: '12px'
}
}
},
markers: { markers: {
size: 6, size: 6,
colors: ['#3b82f6'],
strokeColors: '#fff',
strokeWidth: 2,
hover: { hover: {
size: 8 size: 8
} }
}, },
xaxis: {
categories: vacationsCharts.value.requests_by_month?.map(item => item.month_name) || [],
title: {
text: 'Mes'
}
},
yaxis: {
title: {
text: 'Número de Solicitudes'
},
min: 0
},
tooltip: { tooltip: {
theme: 'dark' y: {
formatter: function (val) {
return val + " solicitudes"
}
}
},
grid: {
borderColor: '#f1f1f1'
} }
})); }));
const lineChartSeries = computed(() => chartData.value.lineChartSeries); // Series para gráfica de solicitudes por mes
const lineChartSeries = computed(() => [{
name: 'Solicitudes',
data: vacationsCharts.value.requests_by_month?.map(item => item.total_requests) || []
}]);
// Datos para el gráfico de dona (Solicitudes por Departamento) - Solo para admin // Opciones para gráfica de solicitudes por departamento (donut)
const donutChartOptions = computed(() => ({ const donutChartOptions = computed(() => ({
chart: { chart: {
type: 'donut', type: 'donut',
height: 350 height: 350
}, },
colors: ['#8b5cf6', '#10b981', '#f59e0b', '#ef4444', '#3b82f6', '#f97316'], colors: ['#3B82F6', '#8B5CF6', '#06B6D4', '#10B981', '#F59E0B', '#EF4444', '#EC4899', '#84CC16'],
labels: chartData.value.donutChartLabels, labels: vacationsCharts.value.requests_by_department?.map(item => item.department_name) || [],
legend: { legend: {
position: 'right', position: 'bottom',
fontSize: '14px', horizontalAlign: 'center'
labels: {
colors: '#64748b'
}
}, },
plotOptions: { plotOptions: {
pie: { pie: {
donut: { donut: {
size: '60%' size: '70%',
labels: {
show: true,
name: {
show: true,
fontSize: '14px',
fontWeight: 600
},
value: {
show: true,
fontSize: '16px',
fontWeight: 700,
formatter: function (val) {
return val + " solicitudes"
}
},
total: {
show: true,
showAlways: false,
label: 'Total',
fontSize: '16px',
fontWeight: 700,
color: '#373d3f',
formatter: function (w) {
return w.globals.seriesTotals.reduce((a, b) => {
return a + b
}, 0) + " solicitudes"
}
}
}
} }
} }
}, },
dataLabels: { tooltip: {
enabled: true, y: {
formatter: function(val, opts) { formatter: function (val) {
return opts.w.config.series[opts.seriesIndex] return val + " solicitudes"
}, }
style: {
fontSize: '14px',
colors: ['#fff']
} }
}, },
tooltip: { responsive: [{
theme: 'dark' breakpoint: 480,
} options: {
chart: {
width: 200
},
legend: {
position: 'bottom'
}
}
}]
})); }));
const donutChartSeries = computed(() => chartData.value.donutChartSeries); // Series para gráfica de solicitudes por departamento
const donutChartSeries = computed(() =>
vacationsCharts.value.requests_by_department?.map(item => item.total_requests) || []
);
/** Ciclos */
onMounted(() => {
api.get(apiTo('admin'), {
onSuccess: (r) => {
adminInfo.value = r.admin_info;
vacationsCharts.value = r.admin_info.charts;
}
});
});
</script> </script>
<template> <template>
@ -216,7 +219,7 @@ const donutChartSeries = computed(() => chartData.value.donutChartSeries);
</section> </section>
<!-- Gráficos --> <!-- Gráficos -->
<section v-if="charts.requests_by_month && charts.requests_by_department" class="grid grid-cols-1 gap-6 lg:grid-cols-2"> <section v-if="vacationsCharts.requests_by_month && vacationsCharts.requests_by_department" class="grid grid-cols-1 gap-6 lg:grid-cols-2">
<!-- Gráfico de Solicitudes por Mes --> <!-- Gráfico de Solicitudes por Mes -->
<article class="rounded-xl bg-white p-6 shadow dark:bg-slate-800"> <article class="rounded-xl bg-white p-6 shadow dark:bg-slate-800">
<div class="mb-6 flex items-center gap-3"> <div class="mb-6 flex items-center gap-3">

View File

@ -1,30 +1,28 @@
<script setup> <script setup>
import { computed } from 'vue'; import { onMounted, ref } from 'vue';
import { api } from '@Services/Api';
import { apiTo } from './Module';
import GoogleIcon from '@Shared/GoogleIcon.vue'; import GoogleIcon from '@Shared/GoogleIcon.vue';
// Props // Propiedades
const props = defineProps({ const vacations = ref({});
vacations: { const vacationsRequests = ref([]);
type: Object, const coordinatorInfo = ref({});
default: () => ({}) const lastVacationRequests = ref([]);
}, const employeeStatusDepartment = ref([]);
vacationsRequests: {
type: Array, /** Ciclos */
default: () => [] onMounted(() => {
}, api.get(apiTo('coordinator'), {
coordinatorInfo: { onSuccess: (r) => {
type: Object, vacations.value = r.vacations;
default: () => ({}) vacationsRequests.value = r.vacation_requests;
}, coordinatorInfo.value = r.coordinator_info;
lastVacationRequests: { lastVacationRequests.value = r.coordinator_info?.last_vacation_requests;
type: Array, employeeStatusDepartment.value = r.coordinator_info?.employee_status_department;
default: () => [] }
}, });
employeeStatusDepartment: {
type: Array,
default: () => []
}
}); });
</script> </script>

View File

@ -1,20 +1,24 @@
<script setup> <script setup>
import { computed } from 'vue'; import { onMounted, ref } from 'vue';
import { api } from '@Services/Api';
import { apiTo } from './Module';
import PrimaryButton from '@Holos/Button/Primary.vue'; import PrimaryButton from '@Holos/Button/Primary.vue';
import GoogleIcon from '@Shared/GoogleIcon.vue'; import GoogleIcon from '@Shared/GoogleIcon.vue';
// Props // Propiedades
const props = defineProps({ const vacations = ref({});
vacations: { const vacationsRequests = ref([]);
type: Object,
default: () => ({}) /** Ciclos */
}, onMounted(() => {
vacationsRequests: { api.get(apiTo('employee'), {
type: Array, onSuccess: (r) => {
default: () => [] vacations.value = r.vacations;
} vacationsRequests.value = r.vacation_requests;
}
});
}); });
</script> </script>

View File

@ -90,12 +90,8 @@ onMounted(() => {
<template> <template>
<div> <div>
<!-- Header --> <!-- Header -->
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between my-4 sm:my-8 gap-3 sm:gap-0"> <h1 class="text-xl sm:text-2xl font-bold text-slate-900 dark:text-slate-100">Gestión de Solicitudes</h1>
<div> <p class="text-sm sm:text-base text-slate-600 dark:text-slate-400">Revisa y gestiona las solicitudes de vacaciones de tu equipo</p>
<h1 class="text-xl sm:text-2xl font-bold text-slate-900 dark:text-slate-100">Gestión de Solicitudes</h1>
<p class="text-sm sm:text-base text-slate-600 dark:text-slate-400">Revisa y gestiona las solicitudes de vacaciones de tu equipo</p>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-12 gap-4 lg:gap-6"> <div class="grid grid-cols-1 lg:grid-cols-12 gap-4 lg:gap-6">
<div class="order-2 lg:order-1 lg:col-span-8"> <div class="order-2 lg:order-1 lg:col-span-8">

View File

@ -1,12 +1,12 @@
<script setup> <script setup>
import { ref, watch, computed } from 'vue'; import { ref, watch, computed } from 'vue';
import VueDatePicker from '@vuepic/vue-datepicker'; import VueDatePicker from '@vuepic/vue-datepicker';
import '@vuepic/vue-datepicker/dist/main.css'; import '@vuepic/vue-datepicker/dist/main.css';
import PrimaryButton from '@Holos/Button/Primary.vue'; import PrimaryButton from '@Holos/Button/Primary.vue';
import Input from '@Holos/Form/Input.vue'; import Textarea from '@Holos/Form/Textarea.vue';
import Textarea from '@Holos/Form/Textarea.vue'; import GoogleIcon from '@Shared/GoogleIcon.vue';
import GoogleIcon from '@Shared/GoogleIcon.vue';
/** Eventos */ /** Eventos */
const emit = defineEmits([ const emit = defineEmits([