휴가관리 수정
This commit is contained in:
parent
fe00b53e4f
commit
f2b364f4f8
@ -5,7 +5,7 @@
|
||||
<button class="close-btn" @click="closeModal">✖</button>
|
||||
|
||||
<div class="modal-body">
|
||||
<p>해당 직원에게 부여할 연차 개수를 선택하세요. (남은 개수: {{ availableQuota }}개)</p>
|
||||
<p>선물할 연차 개수를 선택하세요.</p>
|
||||
|
||||
<div class="vacation-control">
|
||||
<button @click="decreaseCount" :disabled="grantCount < 2" class="count-btn">-</button>
|
||||
@ -129,7 +129,7 @@
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
width: 400px;
|
||||
width: 300px;
|
||||
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.15);
|
||||
text-align: center;
|
||||
position: relative;
|
||||
|
||||
@ -151,7 +151,7 @@ const closeModal = () => {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
width: 400px;
|
||||
width: 300px;
|
||||
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.15);
|
||||
text-align: center;
|
||||
position: relative;
|
||||
@ -222,4 +222,10 @@ const closeModal = () => {
|
||||
color: gray;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* 모달 본문 스크롤: 높이가 300px 이상이면 스크롤바 표시 */
|
||||
.modal-body {
|
||||
max-height: 130px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="card-body d-flex justify-content-center">
|
||||
<ul class="list-unstyled d-flex align-items-center gap-5 mb-0 mt-2">
|
||||
<ul class="list-unstyled d-flex flex-wrap align-items-center gap-2 mb-0 mt-2">
|
||||
<li
|
||||
v-for="(user, index) in sortedUserList"
|
||||
:key="index"
|
||||
@ -34,8 +34,8 @@
|
||||
defineEmits(["profileClick"]);
|
||||
|
||||
defineProps({
|
||||
remainingVacationData: Object,
|
||||
});
|
||||
remainingVacationData: Object,
|
||||
});
|
||||
|
||||
const userStore = useUserInfoStore();
|
||||
const userListStore = useUserStore();
|
||||
@ -47,28 +47,28 @@
|
||||
const userColors = ref({});
|
||||
|
||||
onMounted(async () => {
|
||||
await userStore.userInfo();
|
||||
if (userStore.user) {
|
||||
employeeId.value = userStore.user.id;
|
||||
} else {
|
||||
console.error("❌ 로그인한 사용자 정보를 불러오지 못했습니다.");
|
||||
}
|
||||
await userStore.userInfo();
|
||||
if (userStore.user) {
|
||||
employeeId.value = userStore.user.id;
|
||||
} else {
|
||||
console.error("❌ 로그인한 사용자 정보를 불러오지 못했습니다.");
|
||||
}
|
||||
|
||||
await userListStore.fetchUserList();
|
||||
userList.value = userListStore.userList;
|
||||
await userListStore.fetchUserList();
|
||||
userList.value = userListStore.userList;
|
||||
|
||||
// 사용자별 색상 저장
|
||||
userList.value.forEach(user => {
|
||||
userColors.value[user.MEMBERSEQ] = user.usercolor || "#ccc";
|
||||
});
|
||||
// 사용자별 색상 저장
|
||||
userList.value.forEach(user => {
|
||||
userColors.value[user.MEMBERSEQ] = user.usercolor || "#ccc";
|
||||
});
|
||||
|
||||
nextTick(() => {
|
||||
const tooltips = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||
tooltips.forEach(tooltip => {
|
||||
new bootstrap.Tooltip(tooltip);
|
||||
nextTick(() => {
|
||||
const tooltips = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||
tooltips.forEach(tooltip => {
|
||||
new bootstrap.Tooltip(tooltip);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const sortedUserList = computed(() => {
|
||||
if (!employeeId.value) return userList.value; // 로그인한 사용자가 없으면 기존 리스트 반환
|
||||
@ -78,6 +78,7 @@
|
||||
return myProfile ? [myProfile, ...otherUsers] : userList.value;
|
||||
});
|
||||
|
||||
// 프로필 이미지 URL 반환
|
||||
const getUserProfileImage = (profilePath) => {
|
||||
return profilePath && profilePath.trim()
|
||||
? `${baseUrl}upload/img/profile/${profilePath}`
|
||||
@ -92,14 +93,12 @@
|
||||
event.target.style.visibility = "visible";
|
||||
};
|
||||
|
||||
// 프로필 크기 동적 조정
|
||||
// 프로필 크기 동적 조정: 사이드바 영역에 맞게 조금 더 축소
|
||||
const profileSize = computed(() => {
|
||||
const totalUsers = userList.value.length;
|
||||
|
||||
if (totalUsers <= 7) return "100px"; // 7명 이하
|
||||
if (totalUsers <= 10) return "80px"; // ~10명
|
||||
if (totalUsers <= 20) return "60px"; // ~20명
|
||||
return "40px"; // 20명 이상
|
||||
if (totalUsers <= 10) return "68px"; // ~10명
|
||||
if (totalUsers <= 15) return "50px"; // ~20명
|
||||
return "30px"; // 20명 이상
|
||||
});
|
||||
|
||||
// 개별 유저 스타일 적용
|
||||
@ -109,18 +108,29 @@
|
||||
height: profileSize.value,
|
||||
borderWidth: "4px",
|
||||
borderColor: user.usercolor || "#ccc",
|
||||
borderStyle: "solid",
|
||||
borderStyle: "solid",
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 남은 연차 개수 스타일 */
|
||||
.remaining-vacation {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.remaining-vacation {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* ul에 flex-wrap을 적용하여 넘치는 프로필이 다음 줄로 내려가도록 함 */
|
||||
ul {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* li 간 간격 조정 */
|
||||
li {
|
||||
margin: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,43 +1,51 @@
|
||||
<template>
|
||||
<div class="vacation-management">
|
||||
<div class="container-xxl flex-grow-1 container-p-y">
|
||||
<div class="card app-calendar-wrapper">
|
||||
<div class="row g-0">
|
||||
<div class="col app-calendar-content">
|
||||
<div class="card shadow-none border-0">
|
||||
<ProfileList
|
||||
@profileClick="handleProfileClick"
|
||||
:remainingVacationData="remainingVacationData"
|
||||
/>
|
||||
<div class="card-body w-75 p-3 align-self-center">
|
||||
<!-- 모달에 필터링된 연차 목록 전달 -->
|
||||
<VacationModal
|
||||
v-if="isModalOpen"
|
||||
:isOpen="isModalOpen"
|
||||
:myVacations="filteredMyVacations"
|
||||
:receivedVacations="filteredReceivedVacations"
|
||||
:userColors="userColors"
|
||||
@close="isModalOpen = false"
|
||||
/>
|
||||
<div class="container-xxl flex-grow-1 container-p-y">
|
||||
<div class="card app-calendar-wrapper">
|
||||
<div class="row g-0">
|
||||
<!-- Sidebar: 사이드바 영역 -->
|
||||
<div class="col-3 app-calendar-sidebar border-end" id="app-calendar-sidebar">
|
||||
<!-- 모달들은 화면 오버레이로 동작하므로 사이드바 내부에 두어도 무방 -->
|
||||
<VacationModal
|
||||
v-if="isModalOpen"
|
||||
:isOpen="isModalOpen"
|
||||
:myVacations="filteredMyVacations"
|
||||
:receivedVacations="filteredReceivedVacations"
|
||||
:userColors="userColors"
|
||||
@close="isModalOpen = false"
|
||||
/>
|
||||
<VacationGrantModal
|
||||
v-if="isGrantModalOpen"
|
||||
:isOpen="isGrantModalOpen"
|
||||
:targetUser="selectedUser"
|
||||
:remainingQuota="remainingVacationData[selectedUser?.MEMBERSEQ] || 0"
|
||||
@close="isGrantModalOpen = false"
|
||||
@updateVacation="fetchRemainingVacation"
|
||||
/>
|
||||
<div class="sidebar-content">
|
||||
<!-- 사원 프로필 리스트 -->
|
||||
<ProfileList
|
||||
@profileClick="handleProfileClick"
|
||||
:remainingVacationData="remainingVacationData"
|
||||
/>
|
||||
</div>
|
||||
<div class="sidebar-actions text-center my-3">
|
||||
<!-- 액션 버튼 -->
|
||||
<HalfDayButtons
|
||||
@toggleHalfDay="toggleHalfDay"
|
||||
@addVacationRequests="saveVacationChanges"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VacationGrantModal
|
||||
v-if="isGrantModalOpen"
|
||||
:isOpen="isGrantModalOpen"
|
||||
:targetUser="selectedUser"
|
||||
:remainingQuota="remainingVacationData[selectedUser?.MEMBERSEQ] || 0"
|
||||
@close="isGrantModalOpen = false"
|
||||
@updateVacation="fetchRemainingVacation"
|
||||
/>
|
||||
<full-calendar
|
||||
ref="fullCalendarRef"
|
||||
:options="calendarOptions"
|
||||
class="flatpickr-calendar-only"
|
||||
/>
|
||||
<HalfDayButtons
|
||||
@toggleHalfDay="toggleHalfDay"
|
||||
@addVacationRequests="saveVacationChanges"
|
||||
/>
|
||||
</div>
|
||||
<!-- Main Content: 캘린더 영역 -->
|
||||
<div class="col app-calendar-content">
|
||||
<div class="card shadow-none border-0">
|
||||
<div class="card-body pb-0">
|
||||
<full-calendar
|
||||
ref="fullCalendarRef"
|
||||
:options="calendarOptions"
|
||||
class="flatpickr-calendar-only"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -46,8 +54,9 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, onMounted, nextTick, computed } from "vue";
|
||||
import { reactive, ref, onMounted, nextTick, computed, watch } from "vue";
|
||||
import axios from "@api";
|
||||
import FullCalendar from "@fullcalendar/vue3";
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
@ -71,11 +80,9 @@
|
||||
const receivedVacations = ref([]); // 전체 "받은 연차" 목록
|
||||
const isModalOpen = ref(false);
|
||||
const remainingVacationData = ref({});
|
||||
// 모달을 열 때 기준 연도 (모달에 표시할 연도)
|
||||
const modalYear = ref(new Date().getFullYear());
|
||||
const modalMonth = ref(String(new Date().getMonth() + 1).padStart(2, "0"));
|
||||
const lastRemainingYear = ref(new Date().getFullYear());
|
||||
|
||||
const lastRemainingYear = ref(new Date().getFullYear());
|
||||
const lastRemainingMonth = ref(String(new Date().getMonth() + 1).padStart(2, "0"));
|
||||
const isGrantModalOpen = ref(false);
|
||||
const selectedUser = ref(null);
|
||||
|
||||
@ -108,6 +115,23 @@
|
||||
await fetchRemainingVacation();
|
||||
});
|
||||
|
||||
// lastRemainingYear 값이 변경될 때마다 해당 연도의 연차 내역을 재조회
|
||||
watch(lastRemainingYear, async (newYear, oldYear) => {
|
||||
try {
|
||||
const response = await axios.get(`vacation/history?year=${newYear}`);
|
||||
if (response.status === 200 && response.data) {
|
||||
myVacations.value = response.data.data.usedVacations || [];
|
||||
receivedVacations.value = response.data.data.receivedVacations || [];
|
||||
} else {
|
||||
console.warn("❌ 연차 내역을 불러오지 못했습니다.");
|
||||
myVacations.value = [];
|
||||
receivedVacations.value = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("🚨 연차 데이터 불러오기 실패:", error);
|
||||
}
|
||||
});
|
||||
|
||||
const fetchRemainingVacation = async () => {
|
||||
try {
|
||||
const response = await axios.get("vacation/remaining");
|
||||
@ -126,6 +150,11 @@
|
||||
// 프로필 클릭 시 연차 내역 가져오기
|
||||
const handleProfileClick = async (user) => {
|
||||
try {
|
||||
// 이미 모달이 열려있다면 토글하여 닫음
|
||||
if (isModalOpen.value) {
|
||||
isModalOpen.value = false;
|
||||
return;
|
||||
}
|
||||
if (user.MEMBERSEQ === userStore.user.id) {
|
||||
const year = new Date().getFullYear(); // 현재 연도
|
||||
// 연도 파라미터를 전달하여 전체 연도의 연차 내역을 조회
|
||||
@ -135,12 +164,17 @@ const handleProfileClick = async (user) => {
|
||||
receivedVacations.value = response.data.data.receivedVacations || [];
|
||||
isModalOpen.value = true;
|
||||
// 모달을 열 때 기준 연도 갱신
|
||||
modalYear.value = year;
|
||||
lastRemainingYear.value = year;
|
||||
isGrantModalOpen.value = false;
|
||||
} else {
|
||||
console.warn("❌ 연차 내역을 불러오지 못했습니다.");
|
||||
}
|
||||
} else {
|
||||
// 이미 모달이 열려있다면 토글하여 닫음
|
||||
if (isGrantModalOpen.value) {
|
||||
isGrantModalOpen.value = false;
|
||||
return;
|
||||
}
|
||||
selectedUser.value = user;
|
||||
isGrantModalOpen.value = true;
|
||||
isModalOpen.value = false;
|
||||
@ -187,14 +221,14 @@ const handleProfileClick = async (user) => {
|
||||
return vacationCodeMap.value[typeCode] || "기타";
|
||||
};
|
||||
|
||||
// computed: modalYear와 일치하는 항목만 필터링
|
||||
// computed: lastRemainingYear과과 일치하는 항목만 필터링
|
||||
const filteredMyVacations = computed(() => {
|
||||
const filtered = myVacations.value.filter(vac => {
|
||||
// vac.date가 없으면 vac.LOCVACUDT를 사용하도록 함
|
||||
const dateStr = vac.date || vac.LOCVACUDT;
|
||||
const year = dateStr ? dateStr.split("T")[0].substring(0, 4) : null;
|
||||
console.log("vacation year:", year, "modalYear:", modalYear.value);
|
||||
return year === String(modalYear.value);
|
||||
console.log("vacation year:", year, "lastRemainingYear:", lastRemainingYear.value);
|
||||
return year === String(lastRemainingYear.value);
|
||||
});
|
||||
console.log("filteredMyVacations:", filtered);
|
||||
return filtered;
|
||||
@ -204,8 +238,8 @@ const filteredReceivedVacations = computed(() => {
|
||||
return receivedVacations.value.filter(vac => {
|
||||
const dateStr = vac.date || vac.LOCVACUDT;
|
||||
const year = dateStr ? dateStr.split("T")[0].substring(0, 4) : null;
|
||||
console.log("vacation year:", year, "modalYear:", modalYear.value);
|
||||
return dateStr && year === String(modalYear.value);
|
||||
console.log("vacation year:", year, "lastRemainingYear:", lastRemainingYear.value);
|
||||
return dateStr && year === String(lastRemainingYear.value);
|
||||
});
|
||||
});
|
||||
|
||||
@ -277,12 +311,12 @@ const filteredReceivedVacations = computed(() => {
|
||||
const response = await axios.get(`vacation/list/${year}/${month}`);
|
||||
if (response.status === 200) {
|
||||
const vacationList = response.data;
|
||||
// 모달이 열려 있더라도 전달받은 연도가 기존 modalYear와 다르면 업데이트
|
||||
if (modalYear.value !== year) {
|
||||
// 모달이 열려 있더라도 전달받은 연도가 기존 lastRemainingYear 다르면 업데이트
|
||||
if (lastRemainingYear.value !== year) {
|
||||
myVacations.value = vacationList.filter(
|
||||
(vac) => vac.MEMBERSEQ === userStore.user.id
|
||||
);
|
||||
modalYear.value = year;
|
||||
lastRemainingYear.value = year;
|
||||
// modalMonth는 그대로 유지 (월은 모달 업데이트 조건에서 제외)
|
||||
}
|
||||
// 캘린더 이벤트 매핑
|
||||
@ -359,9 +393,10 @@ const filteredReceivedVacations = computed(() => {
|
||||
}
|
||||
|
||||
async function loadCalendarData(year, month) {
|
||||
if (lastRemainingYear.value !== year) {
|
||||
if (lastRemainingYear.value !== year || lastRemainingMonth.value !== month) {
|
||||
await fetchRemainingVacation();
|
||||
lastRemainingYear.value = year;
|
||||
lastRemainingMonth.value = month;
|
||||
}
|
||||
fetchedEvents.value = [];
|
||||
const [vacationEvents, holidayEvents] = await Promise.all([
|
||||
|
||||
Loading…
Reference in New Issue
Block a user