휴가수정,버튼추가
This commit is contained in:
parent
8e438ff900
commit
fbc578c307
@ -5,30 +5,13 @@
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.notice-row td {
|
||||
/* 게시판리스트 */
|
||||
.bg-label-gray td {
|
||||
color: #DC3545 !important;
|
||||
}
|
||||
.notice-row {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.general-row {
|
||||
background-color: white;
|
||||
color: black;
|
||||
}
|
||||
.clickable-row {
|
||||
cursor: pointer;
|
||||
}
|
||||
.clickable-row:hover {
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
.new-badge {
|
||||
font-size: 0.65rem;
|
||||
padding: 0.2em 0.4em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
||||
/* 휴가*/
|
||||
/* 휴가 */
|
||||
.half-day-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
12
src/components/button/BackBtn.vue
Normal file
12
src/components/button/BackBtn.vue
Normal file
@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<button type="button" class="btn btn-info" @click="$emit('click')">
|
||||
<i class="bx bx-left-arrow-alt"></i>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "BackButton",
|
||||
emits: ["click"],
|
||||
};
|
||||
</script>
|
||||
23
src/components/button/SaveBtn.vue
Normal file
23
src/components/button/SaveBtn.vue
Normal file
@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary ms-1"
|
||||
@click="$emit('click')"
|
||||
:disabled="!isEnabled"
|
||||
>
|
||||
<i class="bx bx-check"></i>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "SaveButton",
|
||||
props: {
|
||||
isEnabled: {
|
||||
type: Boolean,
|
||||
default: true, // 기본적으로 활성화
|
||||
},
|
||||
},
|
||||
emits: ["click"],
|
||||
};
|
||||
</script>
|
||||
95
src/components/modal/VacationGrantModal.vue
Normal file
95
src/components/modal/VacationGrantModal.vue
Normal file
@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div v-if="isOpen" class="modal-dialog" @click.self="closeModal">
|
||||
<div class="modal-content">
|
||||
<h5 class="modal-title">📅 {{ targetUser.name }}님의 연차 부여</h5>
|
||||
<button class="close-btn" @click="closeModal">✖</button>
|
||||
|
||||
<div class="modal-body">
|
||||
<p>해당 직원에게 부여할 연차 개수를 선택하세요. (최대 2개)</p>
|
||||
|
||||
<div class="vacation-item">
|
||||
<button @click="decreaseCount" :disabled="grantCount <= 0">-</button>
|
||||
<span>{{ grantCount }}</span>
|
||||
<button @click="increaseCount" :disabled="grantCount >= maxQuota">+</button>
|
||||
</div>
|
||||
|
||||
<p class="">현재 보유 연차: <strong>{{ remainingQuota }}</strong></p>
|
||||
</div>
|
||||
|
||||
<div class="">
|
||||
<button class="save-btn" @click="saveVacationGrant" :disabled="grantCount === 0">
|
||||
✅ 저장
|
||||
</button>
|
||||
<button class="cancel-btn" @click="closeModal">취소</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineProps, defineEmits, watch } from "vue";
|
||||
import axios from "@api";
|
||||
|
||||
const props = defineProps({
|
||||
isOpen: Boolean, // 모달 상태
|
||||
targetUser: Object, // 선택한 사용자 정보
|
||||
remainingQuota: Number, // 현재 부여된 연차 개수
|
||||
});
|
||||
|
||||
const emit = defineEmits(["close", "updateVacation"]);
|
||||
|
||||
const grantCount = ref(0);
|
||||
const maxQuota = 2; // 최대 부여 가능 개수
|
||||
|
||||
// ✅ 연차 개수 증가
|
||||
const increaseCount = () => {
|
||||
if (grantCount.value < maxQuota) {
|
||||
grantCount.value++;
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ 연차 개수 감소
|
||||
const decreaseCount = () => {
|
||||
if (grantCount.value > 0) {
|
||||
grantCount.value--;
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ 연차 부여 저장 요청
|
||||
const saveVacationGrant = async () => {
|
||||
try {
|
||||
const response = await axios.post("vacation/grant", {
|
||||
senderId: props.targetUser.senderId, // 로그인한 사용자 ID
|
||||
receiverId: props.targetUser.MEMBERSEQ, // 대상 사용자 ID
|
||||
type: "700103", // 연차 (반차 없음)
|
||||
count: grantCount.value, // 선택한 개수
|
||||
});
|
||||
|
||||
if (response.data && response.data.status === "OK") {
|
||||
alert("✅ 연차가 부여되었습니다.");
|
||||
emit("updateVacation"); // ✅ 연차 정보 갱신 요청
|
||||
closeModal();
|
||||
} else {
|
||||
alert("🚨 연차 추가 중 오류가 발생했습니다.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("🚨 연차 추가 실패:", error);
|
||||
alert("연차 추가에 실패했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ 모달 닫기
|
||||
const closeModal = () => {
|
||||
emit("close");
|
||||
};
|
||||
|
||||
// ✅ 모달이 열릴 때 초기 값 설정
|
||||
watch(() => props.isOpen, (newVal) => {
|
||||
if (newVal) {
|
||||
grantCount.value = 0; // 초기화
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@ -87,7 +87,7 @@ align-items: center;
|
||||
|
||||
/* 스크롤 가능한 모달 */
|
||||
.modal-content {
|
||||
max-height: 80vh;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
width: 75%;
|
||||
|
||||
@ -17,6 +17,9 @@
|
||||
@error="setDefaultImage"
|
||||
@load="showImage"
|
||||
/>
|
||||
<span class="remaining-vacation">
|
||||
{{ remainingVacationData[user.MEMBERSEQ] || 0 }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@ -30,6 +33,10 @@
|
||||
|
||||
defineEmits(["profileClick"]);
|
||||
|
||||
defineProps({
|
||||
remainingVacationData: Object,
|
||||
});
|
||||
|
||||
const userStore = useUserStore();
|
||||
const userListStore = useUserListStore();
|
||||
|
||||
@ -53,7 +60,6 @@
|
||||
// 사용자별 색상 저장
|
||||
userList.value.forEach(user => {
|
||||
userColors.value[user.MEMBERSEQ] = user.usercolor || "#ccc";
|
||||
console.log(userColors.value[user.MEMBERSEQ])
|
||||
});
|
||||
|
||||
nextTick(() => {
|
||||
@ -109,4 +115,12 @@
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 남은 연차 개수 스타일 */
|
||||
.remaining-vacation {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-top: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -55,14 +55,14 @@
|
||||
<template v-if="pagination.currentPage === 1 && !showNotices">
|
||||
<tr v-for="(notice, index) in noticeList"
|
||||
:key="'notice-' + index"
|
||||
class="notice-row clickable-row"
|
||||
class="bg-label-gray"
|
||||
@click="goDetail(notice.id)">
|
||||
<td>공지</td>
|
||||
<td>
|
||||
📌 {{ notice.title }}
|
||||
<i v-if="notice.img" class="bi bi-image me-1"></i>
|
||||
<i v-if="notice.hasAttachment" class="bi bi-paperclip"></i>
|
||||
<span v-if="isNewPost(notice.date)" class="badge bg-danger text-white ms-2 new-badge">N</span>
|
||||
<span v-if="isNewPost(notice.date)" class="badge bg-danger text-white ms-2 fs-tiny">N</span>
|
||||
</td>
|
||||
<td>{{ notice.author }}</td>
|
||||
<td>{{ notice.date }}</td>
|
||||
@ -72,14 +72,14 @@
|
||||
<!-- 일반 게시물 -->
|
||||
<tr v-for="(post, index) in generalList"
|
||||
:key="'post-' + index"
|
||||
class="general-row clickable-row"
|
||||
class="invert-bg-white"
|
||||
@click="goDetail(post.realId)">
|
||||
<td>{{ post.id }}</td>
|
||||
<td>
|
||||
{{ post.title }}
|
||||
<i v-if="post.img" class="bi bi-image me-1"></i>
|
||||
<i v-if="post.hasAttachment" class="bi bi-paperclip"></i>
|
||||
<span v-if="isNewPost(post.date)" class="badge bg-danger text-white ms-2 new-badge">N</span>
|
||||
<span v-if="isNewPost(post.date)" class="badge bg-danger text-white ms-2 fs-tiny">N</span>
|
||||
</td>
|
||||
<td>{{ post.author }}</td>
|
||||
<td>{{ post.date }}</td>
|
||||
|
||||
@ -73,17 +73,8 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-4 d-flex justify-content-end">
|
||||
<button type="button" class="btn btn-info" @click="goList">
|
||||
<i class="bx bx-left-arrow-alt"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary ms-1"
|
||||
@click="write"
|
||||
:disabled="!isFileValid"
|
||||
>
|
||||
<i class="bx bx-check"></i>
|
||||
</button>
|
||||
<BackButton @click="goList" />
|
||||
<SaveButton @click="write" :isEnabled="isFileValid" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -98,6 +89,8 @@ import FormFile from '@c/input/FormFile.vue';
|
||||
import { getCurrentInstance, ref, onMounted } from 'vue';
|
||||
import router from '@/router';
|
||||
import axios from '@api';
|
||||
import SaveButton from '@c/button/SaveBtn.vue';
|
||||
import BackButton from '@c/button/BackBtn.vue'
|
||||
|
||||
const categoryList = ref([]);
|
||||
const title = ref('');
|
||||
|
||||
@ -5,7 +5,10 @@
|
||||
<div class="row g-0">
|
||||
<div class="col app-calendar-content">
|
||||
<div class="card shadow-none border-0">
|
||||
<ProfileList @profileClick="handleProfileClick" />
|
||||
<ProfileList
|
||||
@profileClick="handleProfileClick"
|
||||
:remainingVacationData="remainingVacationData"
|
||||
/>
|
||||
<div class="card-body">
|
||||
<VacationModal
|
||||
v-if="isModalOpen"
|
||||
@ -16,6 +19,14 @@
|
||||
@click="handleProfileClick(user)"
|
||||
@close="isModalOpen = false"
|
||||
/>
|
||||
<VacationGrantModal
|
||||
v-if="isGrantModalOpen"
|
||||
:isOpen="isGrantModalOpen"
|
||||
:targetUser="selectedUser"
|
||||
:remainingQuota="remainingVacationData[selectedUser?.MEMBERSEQ] || 0"
|
||||
@close="isGrantModalOpen = false"
|
||||
@updateVacation="fetchRemainingVacation"
|
||||
/>
|
||||
<full-calendar
|
||||
ref="fullCalendarRef"
|
||||
:options="calendarOptions"
|
||||
@ -40,40 +51,70 @@ import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
import interactionPlugin from "@fullcalendar/interaction";
|
||||
import "flatpickr/dist/flatpickr.min.css";
|
||||
import "@/assets/css/app-calendar.css";
|
||||
import { reactive, ref, onMounted, nextTick, computed } from "vue";
|
||||
import { reactive, ref, onMounted, nextTick } from "vue";
|
||||
import axios from "@api";
|
||||
import "bootstrap-icons/font/bootstrap-icons.css";
|
||||
import HalfDayButtons from "@c/button/HalfDayButtons.vue";
|
||||
import ProfileList from "@c/vacation/ProfileList.vue";
|
||||
import { useUserStore } from "@s/userList";
|
||||
import VacationModal from "@c/modal/VacationModal.vue"
|
||||
import { useUserStore as useUserListStore } from "@s/userList";
|
||||
import VacationModal from "@c/modal/VacationModal.vue"
|
||||
import { useUserStore } from "@s/useUserStore";
|
||||
import VacationGrantModal from "@c/modal/VacationGrantModal.vue";
|
||||
|
||||
const userStore = useUserStore();
|
||||
const userListStore = useUserListStore();
|
||||
const userList = ref([]);
|
||||
const userColors = ref({});
|
||||
const myVacations = ref([]); // 내가 사용한 연차 목록
|
||||
const receivedVacations = ref([]); // 내가 받은 연차 목록
|
||||
const isModalOpen = ref(false);
|
||||
const remainingVacationData = ref({});
|
||||
|
||||
const isGrantModalOpen = ref(false);
|
||||
const selectedUser = ref(null);
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
await userStore.userInfo();
|
||||
await fetchRemainingVacation();
|
||||
});
|
||||
|
||||
const fetchRemainingVacation = async () => {
|
||||
try {
|
||||
const response = await axios.get("vacation/remaining");
|
||||
if (response.status === 200) {
|
||||
console.log("✅ 남은 연차 데이터:", response.data);
|
||||
remainingVacationData.value = response.data.data.reduce((acc, vacation) => {
|
||||
acc[vacation.employeeId] = vacation.remainingQuota;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("🚨 남은 연차 데이터를 불러오지 못했습니다:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 프로필 클릭 시 연차 내역 가져오기
|
||||
const handleProfileClick = async (user) => {
|
||||
try {
|
||||
console.log(`🔍 ${user.MEMBERSEQ}님의 연차 내역 요청 중...`);
|
||||
if (user.MEMBERSEQ === userStore.user.id) {
|
||||
// 내 프로필을 클릭한 경우
|
||||
const response = await axios.get(`vacation/history`);
|
||||
|
||||
if (response.status === 200 && response.data) {
|
||||
console.log("✅ 연차 내역 응답:", response.data);
|
||||
myVacations.value = response.data.data.usedVacations || [];
|
||||
receivedVacations.value = response.data.data.receivedVacations || [];
|
||||
|
||||
console.log("📌 myVacations:", myVacations.value);
|
||||
console.log("📌 receivedVacations:", receivedVacations.value);
|
||||
|
||||
isModalOpen.value = true;
|
||||
isModalOpen.value = true; // 내 연차 모달 열기
|
||||
isGrantModalOpen.value = false;
|
||||
} else {
|
||||
console.warn("❌ 연차 내역을 불러오지 못했습니다.");
|
||||
}
|
||||
} else {
|
||||
// 다른 사람의 프로필을 클릭한 경우
|
||||
selectedUser.value = user;
|
||||
isGrantModalOpen.value = true; // 연차 부여 모달 열기
|
||||
isModalOpen.value = false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("🚨 연차 데이터 불러오기 실패:", error);
|
||||
}
|
||||
@ -81,8 +122,8 @@ const handleProfileClick = async (user) => {
|
||||
|
||||
const fetchUserList = async () => {
|
||||
try {
|
||||
await userStore.fetchUserList();
|
||||
userList.value = userStore.userList;
|
||||
await userListStore.fetchUserList();
|
||||
userList.value = userListStore.userList;
|
||||
|
||||
if (!userList.value.length) {
|
||||
console.warn("📌 사용자 목록이 비어 있음!");
|
||||
@ -104,6 +145,7 @@ const calendarEvents = ref([]); // 최종적으로 FullCalendar에 표시할 이
|
||||
const fetchedEvents = ref([]); // API에서 불러온 이벤트 (휴가, 공휴일)
|
||||
const selectedDates = ref(new Map()); // 사용자가 클릭한 날짜 및 타입
|
||||
const halfDayType = ref(null);
|
||||
const vacationCodeMap = ref({}); // 휴가 코드명 저장용
|
||||
|
||||
// 공휴일 날짜(YYYY-MM-DD 형식)를 저장 (클릭 불가 처리용)
|
||||
const holidayDates = ref(new Set());
|
||||
@ -124,6 +166,28 @@ datesSet: handleMonthChange,
|
||||
events: calendarEvents,
|
||||
});
|
||||
|
||||
const fetchVacationCodes = async () => {
|
||||
try {
|
||||
const response = await axios.get("vacation/codes");
|
||||
if (response.status === 200 && response.data) {
|
||||
// 배열을 객체 형태로 변환
|
||||
vacationCodeMap.value = response.data.data.reduce((acc, item) => {
|
||||
acc[item.code] = item.name; // code를 key로, name을 value로 설정
|
||||
return acc;
|
||||
}, {});
|
||||
} else {
|
||||
console.warn("❌ 공통 코드 데이터를 불러오지 못했습니다.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("🚨 공통 코드 API 호출 실패:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 🔹 typeCode를 통해 code명 반환
|
||||
const getVacationType = (typeCode) => {
|
||||
return vacationCodeMap.value[typeCode] || "기타";
|
||||
};
|
||||
|
||||
/**
|
||||
* API 이벤트(fetchedEvents)와 사용자가 선택한 날짜(selectedDates)를 병합하여
|
||||
* calendarEvents를 업데이트하는 함수
|
||||
@ -132,29 +196,26 @@ events: calendarEvents,
|
||||
*/
|
||||
function updateCalendarEvents() {
|
||||
const selectedEvents = Array.from(selectedDates.value).map(([date, type]) => {
|
||||
let className = "";
|
||||
let title = "";
|
||||
if (type === "D") {
|
||||
className = "selected-am"; // 오전: 왼쪽 절반
|
||||
title = "오전반차 (선택)";
|
||||
} else if (type === "N") {
|
||||
className = "selected-pm"; // 오후: 오른쪽 절반
|
||||
title = "오후반차 (선택)";
|
||||
} else {
|
||||
className = "selected-full"; // 전체 영역
|
||||
title = "연차 (선택)";
|
||||
}
|
||||
return {
|
||||
title,
|
||||
title: getVacationType(type),
|
||||
start: date,
|
||||
backgroundColor: "rgba(0, 128, 0, 0.3)",
|
||||
display: "background",
|
||||
classNames: [className],
|
||||
classNames: [getVacationTypeClass(type)],
|
||||
};
|
||||
});
|
||||
calendarEvents.value = [...fetchedEvents.value, ...selectedEvents];
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ 반차 유형에 따라 클래스명 지정 (색상 변경 없이 영역만 조정)
|
||||
*/
|
||||
const getVacationTypeClass = (type) => {
|
||||
if (type === "700101") return "half-day-am"; // 오전반차 → 왼쪽 절반
|
||||
if (type === "700102") return "half-day-pm"; // 오후반차 → 오른쪽 절반
|
||||
return "full-day"; // 연차 → 전체 배경
|
||||
};
|
||||
|
||||
/**
|
||||
* 날짜 클릭 이벤트
|
||||
* - 주말(토, 일)과 공휴일은 클릭되지 않음
|
||||
@ -175,9 +236,9 @@ if (holidayDates.value.has(clickedDateStr)) {
|
||||
if (!selectedDates.value.has(clickedDateStr)) {
|
||||
const type = halfDayType.value
|
||||
? halfDayType.value === "AM"
|
||||
? "D"
|
||||
: "N"
|
||||
: "F";
|
||||
? "700101"
|
||||
: "700102"
|
||||
: "700103";
|
||||
selectedDates.value.set(clickedDateStr, type);
|
||||
} else {
|
||||
selectedDates.value.delete(clickedDateStr);
|
||||
@ -206,22 +267,11 @@ try {
|
||||
let dateStr = vac.LOCVACUDT.split("T")[0];
|
||||
let className = "fc-daygrid-event";
|
||||
let backgroundColor = userColors.value[vac.MEMBERSEQ] || "#FFFFFF";
|
||||
let title = "연차";
|
||||
if (vac.LOCVACTYP === "D") {
|
||||
title = "오전반차";
|
||||
className += " half-day-am";
|
||||
} else if (vac.LOCVACTYP === "N") {
|
||||
title = "오후반차";
|
||||
className += " half-day-pm";
|
||||
} else if (vac.LOCVACTYP === "F") {
|
||||
title = "연차";
|
||||
className += " full-day";
|
||||
}
|
||||
return {
|
||||
title,
|
||||
title: getVacationType(vac.LOCVACTYP),
|
||||
start: dateStr,
|
||||
backgroundColor,
|
||||
classNames: [className],
|
||||
classNames: [getVacationTypeClass(vac.LOCVACTYP)],
|
||||
};
|
||||
})
|
||||
.filter((event) => event !== null);
|
||||
@ -252,6 +302,7 @@ try {
|
||||
const response = await axios.post("vacation", vacationRequests);
|
||||
if (response.data && response.data.status === "OK") {
|
||||
alert("휴가가 저장되었습니다.");
|
||||
await fetchRemainingVacation();
|
||||
// 저장 후 현재 달 데이터 다시 불러오기
|
||||
const currentDate = fullCalendarRef.value.getApi().getDate();
|
||||
const year = currentDate.getFullYear();
|
||||
@ -317,6 +368,7 @@ fullCalendarRef.value.getApi().refetchEvents();
|
||||
// 컴포넌트 마운트 시 현재 달의 데이터 로드
|
||||
onMounted(async () => {
|
||||
await fetchUserList(); // 사용자 목록 먼저 불러오기
|
||||
await fetchVacationCodes();
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = String(today.getMonth() + 1).padStart(2, "0");
|
||||
|
||||
Loading…
Reference in New Issue
Block a user