340 lines
18 KiB
Vue
340 lines
18 KiB
Vue
<script setup>
|
|
import { onMounted, ref } from 'vue';
|
|
import { api } from '@Services/Api';
|
|
import { apiTo } from './Module';
|
|
|
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
|
|
|
// Propiedades
|
|
const vacations = ref({});
|
|
const vacationsRequests = ref([]);
|
|
const coordinatorInfo = ref({});
|
|
const lastVacationRequests = ref([]);
|
|
const employeeStatusDepartment = ref([]);
|
|
|
|
/** Ciclos */
|
|
onMounted(() => {
|
|
api.get(apiTo('coordinator'), {
|
|
onSuccess: (r) => {
|
|
vacations.value = r.vacations;
|
|
vacationsRequests.value = r.vacation_requests;
|
|
coordinatorInfo.value = r.coordinator_info;
|
|
lastVacationRequests.value = r.coordinator_info?.last_vacation_requests;
|
|
employeeStatusDepartment.value = r.coordinator_info?.employee_status_department;
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<!-- Banner de bienvenida -->
|
|
<section
|
|
class="relative mb-8 overflow-hidden rounded-2xl bg-gradient-to-r from-purple-600 to-indigo-600 text-white"
|
|
>
|
|
<div class="relative z-10 p-6 md:p-8">
|
|
<p class="text-2xl md:text-3xl font-semibold">
|
|
Bienvenido, {{ $page.user.name }}
|
|
</p>
|
|
<p class="mt-2 text-white/90">
|
|
Coordina y gestiona las solicitudes de vacaciones de tu equipo.
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
<h2 class="mb-6 text-xl font-bold text-slate-900 dark:text-slate-100">
|
|
Gestión del Equipo
|
|
</h2>
|
|
|
|
<!-- Tarjetas de KPIs -->
|
|
<section class="mb-8 grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-4">
|
|
<!-- Solicitudes Pendientes -->
|
|
<article class="rounded-xl bg-white p-6 shadow with-transition hover:shadow-lg dark:bg-slate-800">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-4">
|
|
<GoogleIcon name="schedule" class="text-amber-500 text-5xl" />
|
|
<div>
|
|
<h3 class="text-sm text-slate-500 dark:text-slate-400">Solicitudes Pendientes</h3>
|
|
<p class="text-2xl font-bold text-slate-900 dark:text-slate-100">{{ coordinatorInfo.pending_requests }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
|
|
<!-- Conflictos Detectados -->
|
|
<article class="rounded-xl bg-white p-6 shadow with-transition hover:shadow-lg dark:bg-slate-800">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-4">
|
|
<GoogleIcon name="warning" class="text-red-500 text-5xl" />
|
|
<div>
|
|
<h3 class="text-sm text-slate-500 dark:text-slate-400">Conflictos Detectados</h3>
|
|
<p class="text-2xl font-bold text-slate-900 dark:text-slate-100">{{ coordinatorInfo.conflicts }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
|
|
<!-- Miembros del Equipo -->
|
|
<article class="rounded-xl bg-white p-6 shadow with-transition hover:shadow-lg dark:bg-slate-800">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-4">
|
|
<GoogleIcon name="group" class="text-blue-500 text-5xl" />
|
|
<div>
|
|
<h3 class="text-sm text-slate-500 dark:text-slate-400">Miembros del Equipo</h3>
|
|
<p class="text-2xl font-bold text-slate-900 dark:text-slate-100">{{ coordinatorInfo.department_employees }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
|
|
<!-- Aprobadas Este Mes -->
|
|
<article class="rounded-xl bg-white p-6 shadow with-transition hover:shadow-lg dark:bg-slate-800">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-4">
|
|
<GoogleIcon name="check_circle" class="text-emerald-500 text-5xl" />
|
|
<div>
|
|
<h3 class="text-sm text-slate-500 dark:text-slate-400">Aprobadas Este Mes</h3>
|
|
<p class="text-2xl font-bold text-slate-900 dark:text-slate-100">{{ coordinatorInfo.approved_requests_current_month }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
</section>
|
|
|
|
<!-- Contenido principal -->
|
|
<section class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
|
<!-- Solicitudes Pendientes -->
|
|
<article class="rounded-xl bg-white p-6 shadow dark:bg-slate-800">
|
|
<div class="mb-6 flex items-center gap-3">
|
|
<GoogleIcon name="schedule" class="text-amber-500 text-2xl" />
|
|
<div>
|
|
<h3 class="text-lg font-semibold text-slate-900 dark:text-slate-100">Solicitudes Pendientes</h3>
|
|
<p class="text-sm text-slate-500 dark:text-slate-400">Solicitudes que requieren tu aprobación</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="lastVacationRequests.length > 0" class="space-y-4">
|
|
<div
|
|
v-for="request in lastVacationRequests"
|
|
:key="request.user.id"
|
|
class="rounded-lg border border-slate-200 p-4 dark:border-slate-700"
|
|
>
|
|
<div class="mb-3 flex items-center justify-between">
|
|
<div>
|
|
<p class="font-medium text-slate-900 dark:text-slate-100">{{ request.user.name }}</p>
|
|
<p class="text-sm text-slate-500 dark:text-slate-400">
|
|
{{ request.number_of_days }} día{{ request.number_of_days !== 1 ? 's' : '' }}
|
|
({{ request.count_periods }} período{{ request.count_periods !== 1 ? 's' : '' }})
|
|
</p>
|
|
</div>
|
|
<span v-if="request.have_conflict" class="rounded-full bg-red-100 px-3 py-1 text-xs font-medium text-red-700 dark:bg-red-500/10 dark:text-red-400">
|
|
Conflicto
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Mensaje de conflicto -->
|
|
<div v-if="request.have_conflict" class="mb-4 rounded-md bg-red-50 p-3 dark:bg-red-500/10">
|
|
<p class="text-sm text-red-700 dark:text-red-400">Hay conflicto en alguno de los periodos de vacaciones</p>
|
|
</div>
|
|
|
|
<!-- Botones de acción -->
|
|
<div class="flex gap-2">
|
|
<button v-if="!request.have_conflict" class="flex items-center gap-1 rounded-md bg-emerald-600 px-3 py-2 text-sm font-medium text-white hover:bg-emerald-700 with-transition">
|
|
<GoogleIcon name="check" class="text-sm" />
|
|
Aprobar
|
|
</button>
|
|
<button v-if="!request.have_conflict" class="flex items-center gap-1 rounded-md bg-red-600 px-3 py-2 text-sm font-medium text-white hover:bg-red-700 with-transition">
|
|
<GoogleIcon name="close" class="text-sm" />
|
|
Rechazar
|
|
</button>
|
|
<button v-if="request.have_conflict" class="flex items-center gap-1 rounded-md bg-blue-600 px-3 py-2 text-sm font-medium text-white hover:bg-blue-700 with-transition">
|
|
<GoogleIcon name="visibility" class="text-sm" />
|
|
Revisar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="text-center py-8">
|
|
<GoogleIcon name="check_circle" class="text-emerald-400 text-4xl mx-auto mb-2" />
|
|
<p class="text-slate-500 dark:text-slate-400">No hay solicitudes pendientes</p>
|
|
</div>
|
|
|
|
<!-- Ver todas las solicitudes -->
|
|
<div class="mt-4 text-center">
|
|
<button class="inline-flex items-center gap-1 text-sm font-medium text-sky-600 hover:underline dark:text-sky-400">
|
|
Ver todas las solicitudes
|
|
<GoogleIcon name="chevron_right" />
|
|
</button>
|
|
</div>
|
|
</article>
|
|
|
|
<!-- Mi Equipo -->
|
|
<article class="rounded-xl bg-white p-6 shadow dark:bg-slate-800">
|
|
<div class="mb-6 flex items-center gap-3">
|
|
<GoogleIcon name="group" class="text-blue-500 text-2xl" />
|
|
<div>
|
|
<h3 class="text-lg font-semibold text-slate-900 dark:text-slate-100">Mi Equipo</h3>
|
|
<p class="text-sm text-slate-500 dark:text-slate-400">Estado actual de los miembros de tu equipo</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="employeeStatusDepartment.length > 0" class="space-y-3">
|
|
<div
|
|
v-for="employee in employeeStatusDepartment"
|
|
:key="employee.id"
|
|
class="flex items-center justify-between rounded-lg border border-slate-200 p-4 dark:border-slate-700"
|
|
>
|
|
<div>
|
|
<p class="font-medium text-slate-900 dark:text-slate-100">{{ employee.name }}</p>
|
|
<p class="text-sm text-slate-500 dark:text-slate-400">
|
|
{{ employee.vacation_days_available }} día{{ employee.vacation_days_available !== 1 ? 's' : '' }} disponibles
|
|
</p>
|
|
</div>
|
|
<span
|
|
:class="{
|
|
'rounded-full px-3 py-1 text-xs font-medium': true,
|
|
'bg-blue-100 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400': employee.status_ek == 'Disponible',
|
|
'bg-amber-100 text-amber-700 dark:bg-amber-500/10 dark:text-amber-400': employee.status_ek == 'En vacaciones'
|
|
}"
|
|
>
|
|
{{ employee.status_ek }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="text-center py-8">
|
|
<GoogleIcon name="group_off" class="text-slate-400 text-4xl mx-auto mb-2" />
|
|
<p class="text-slate-500 dark:text-slate-400">No hay miembros en el equipo</p>
|
|
</div>
|
|
|
|
<!-- Gestionar equipo -->
|
|
<div class="mt-4 text-center">
|
|
<button class="inline-flex items-center gap-1 text-sm font-medium text-sky-600 hover:underline dark:text-sky-400">
|
|
Gestionar equipo
|
|
<GoogleIcon name="chevron_right" />
|
|
</button>
|
|
</div>
|
|
</article>
|
|
</section>
|
|
|
|
<!-- Mi Información de Vacaciones -->
|
|
|
|
<h2 class="mt-8A text-xl font-bold text-slate-900 dark:text-slate-100">
|
|
Mis Vacaciones Personales
|
|
</h2>
|
|
<section class="mt-8 grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-4">
|
|
<!-- Días disponibles -->
|
|
<article class="rounded-xl bg-white p-6 shadow with-transition hover:shadow-lg dark:bg-slate-800">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-4">
|
|
<GoogleIcon name="check_circle" class="text-emerald-500 text-5xl" />
|
|
<div>
|
|
<h3 class="text-sm text-slate-500 dark:text-slate-400">
|
|
Mis Días Disponibles
|
|
</h3>
|
|
<p class="text-2xl font-bold text-slate-900 dark:text-slate-100">
|
|
{{ vacations.available_days }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<span class="rounded-full bg-emerald-100 px-2 py-1 text-xs font-medium text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-400">
|
|
Disponible
|
|
</span>
|
|
</div>
|
|
</article>
|
|
|
|
<!-- Próxima renovación -->
|
|
<article class="rounded-xl bg-white p-6 shadow with-transition hover:shadow-lg dark:bg-slate-800">
|
|
<div class="flex items-center gap-4">
|
|
<GoogleIcon name="event" class="text-sky-500 text-5xl" />
|
|
<div>
|
|
<h3 class="text-sm text-slate-500 dark:text-slate-400">
|
|
Próxima Renovación
|
|
</h3>
|
|
<div class="flex items-end gap-2">
|
|
<p class="text-2xl font-bold text-slate-900 dark:text-slate-100">
|
|
{{ vacations.next_renewal_date ? (() => { const date = new Date(vacations.next_renewal_date + 'T00:00:00'); return `${date.getDate()}-${date.toLocaleDateString('es-ES', { month: 'long' })}-${date.getFullYear()}` })() : 'N/A' }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
|
|
<!-- Solicitudes activas -->
|
|
<article class="rounded-xl bg-white p-6 shadow with-transition hover:shadow-lg dark:bg-slate-800">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-4">
|
|
<GoogleIcon name="hourglass_top" class="text-amber-500 text-5xl" />
|
|
<div>
|
|
<h3 class="text-sm text-slate-500 dark:text-slate-400">
|
|
Mis Solicitudes Activas
|
|
</h3>
|
|
<p class="text-2xl font-bold text-slate-900 dark:text-slate-100">
|
|
{{ vacations.active_requests }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<span class="rounded-full bg-amber-100 px-2 py-1 text-xs font-medium text-amber-700 dark:bg-amber-500/10 dark:text-amber-400">
|
|
Pendientes
|
|
</span>
|
|
</div>
|
|
</article>
|
|
|
|
<!-- Días usados -->
|
|
<article class="rounded-xl bg-white p-6 shadow with-transition hover:shadow-lg dark:bg-slate-800">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-4">
|
|
<GoogleIcon name="task_alt" class="text-indigo-500 text-5xl" />
|
|
<div>
|
|
<h3 class="text-sm text-slate-500 dark:text-slate-400">Mis Días Usados</h3>
|
|
<p class="text-2xl font-bold text-slate-900 dark:text-slate-100">{{ vacations.used_days }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
</section>
|
|
|
|
<!-- Mis Solicitudes Recientes -->
|
|
<section class="mt-8 rounded-2xl bg-white p-6 shadow dark:bg-slate-800">
|
|
<h3 class="mb-4 text-lg font-semibold text-slate-900 dark:text-slate-100">
|
|
Mis Solicitudes Recientes
|
|
</h3>
|
|
|
|
<div v-if="vacationsRequests?.length == 0" class="text-center py-8">
|
|
<GoogleIcon name="inbox" class="text-slate-400 text-4xl mx-auto mb-2" />
|
|
<p class="text-slate-500 dark:text-slate-400">No tienes solicitudes recientes</p>
|
|
</div>
|
|
|
|
<div v-if="vacationsRequests?.length > 0" class="space-y-3">
|
|
<div
|
|
v-for="request in vacationsRequests"
|
|
class="flex items-center justify-between rounded-lg border border-slate-200 p-4 dark:border-slate-700"
|
|
>
|
|
<div>
|
|
<p class="font-medium">{{ request.first_period_start }} - {{ request.last_period_end }}</p>
|
|
<p class="text-xs text-slate-500 dark:text-slate-400">{{ request.total_days }} días</p>
|
|
</div>
|
|
<span
|
|
:class="{
|
|
'rounded-full px-3 py-1 text-xs font-medium': true,
|
|
'bg-amber-100 text-amber-700 dark:bg-amber-500/10 dark:text-amber-400': request.status === 'pendiente',
|
|
'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-400': request.status === 'aprobada',
|
|
'bg-red-100 text-red-700 dark:bg-red-500/10 dark:text-red-400': request.status === 'rechazada'
|
|
}"
|
|
>
|
|
{{ request.status }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4">
|
|
<RouterLink to="/vacations" class="inline-flex items-center gap-1 text-sm font-medium text-sky-600 hover:underline dark:text-sky-400">
|
|
Ver todas mis solicitudes
|
|
<GoogleIcon name="chevron_right" />
|
|
</RouterLink>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</template>
|