휴가수정,버튼추가

This commit is contained in:
dyhj625 2025-02-14 14:50:23 +09:00
parent 8e438ff900
commit fbc578c307
9 changed files with 257 additions and 85 deletions

View File

@ -5,27 +5,10 @@
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;
}
/* 휴가 */

View 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>

View 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>

View 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>

View File

@ -87,7 +87,7 @@ align-items: center;
/* 스크롤 가능한 모달 */
.modal-content {
max-height: 80vh;
max-height: 60vh;
overflow-y: auto;
padding: 20px;
width: 75%;

View File

@ -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>

View File

@ -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>

View File

@ -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('');

View File

@ -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 { 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");