
--- client/resources/api/vcatn.js
+++ client/resources/api/vcatn.js
... | ... | @@ -1,36 +1,46 @@ |
1 |
-import {apiClient} from "./index"; |
|
1 |
+import { apiClient } from "./index"; |
|
2 | 2 |
|
3 |
-// 등록 |
|
3 |
+// 휴가 등록 |
|
4 | 4 |
export const saveVcatnProc = data => { |
5 | 5 |
return apiClient.post('/vcatn/saveVcatn.json', data); |
6 | 6 |
} |
7 | 7 |
|
8 |
-// 조회 - 현황 |
|
9 |
-export const findVcatnsSummary = () => { |
|
10 |
- return apiClient.get('/vcatn/findVcatnsSummary.json'); |
|
8 |
+// 휴가 현황 조회 |
|
9 |
+export const findVcatnSummaryProc = () => { |
|
10 |
+ return apiClient.get('/vcatn/findVcatnSummary.json'); |
|
11 | 11 |
} |
12 | 12 |
|
13 |
-// 조회 - 목록 |
|
13 |
+// 휴가 목록 조회 |
|
14 | 14 |
export const findVcatnsProc = data => { |
15 | 15 |
return apiClient.get('/vcatn/findVcatns.json', { params: data }); |
16 | 16 |
} |
17 | 17 |
|
18 |
-// 조회 - 상세 |
|
18 |
+// 휴가 상세 조회 |
|
19 | 19 |
export const findVcatnProc = data => { |
20 | 20 |
return apiClient.get(`/vcatn/${data}/findVcatn.json`); |
21 | 21 |
} |
22 | 22 |
|
23 |
-// 조회 - 이전 승인자 |
|
24 |
-export const findLastVcatnProc = () => { |
|
25 |
- return apiClient.get('/vcatn/findLastVcatn.json'); |
|
23 |
+// 휴가 수정 |
|
24 |
+export const updateVcatnProc = (id, data) => { |
|
25 |
+ return apiClient.put(`/vcatn/${id}/updateVcatn.json`, data); |
|
26 | 26 |
} |
27 | 27 |
|
28 |
-// 삭제 |
|
28 |
+// 휴가 삭제 |
|
29 | 29 |
export const deleteVcatnProc = data => { |
30 | 30 |
return apiClient.delete(`/vcatn/${data}/deleteVcatn.json`); |
31 | 31 |
} |
32 | 32 |
|
33 |
-// 수정 |
|
34 |
-export const updateVcatnProc = data => { |
|
35 |
- return apiClient.post('/vcatn/updateVcatn.json', data); |
|
33 |
+// 휴가 이전 승인자 조회 |
|
34 |
+export const findLastVcatnProc = () => { |
|
35 |
+ return apiClient.get('/vcatn/findLastVcatn.json'); |
|
36 |
+} |
|
37 |
+ |
|
38 |
+// 휴가 공통코드 조회 |
|
39 |
+export const findVcatnKndsProc = () => { |
|
40 |
+ return apiClient.get('/vcatn/findVcatnKnds.json'); |
|
41 |
+} |
|
42 |
+ |
|
43 |
+// 휴가 재신청 |
|
44 |
+export const reapplyVcatnProc = (id, data) => { |
|
45 |
+ return apiClient.post(`/vcatn/${id}/reapplyVcatn.json`, data); |
|
36 | 46 |
}(파일 끝에 줄바꿈 문자 없음) |
--- client/resources/js/cmmnPlugin.js
+++ client/resources/js/cmmnPlugin.js
... | ... | @@ -116,46 +116,5 @@ |
116 | 116 |
|
117 | 117 |
return lists; |
118 | 118 |
}; |
119 |
- |
|
120 |
- // 기본 코드 목록 초기화 |
|
121 |
- Vue.config.globalProperties.$defaultCodes = async () => { |
|
122 |
- const codeGroups = { |
|
123 |
- vcatnKndCodeList: [], // 휴가 종류 (연차/반차) |
|
124 |
- bsrpCodeList: [], // 출장 종류 (해외/국내) |
|
125 |
- sanctnCodeList: [], // 결재 구분 (결재/대결/전결) |
|
126 |
- confmCodeList: [], // 상태 코드 (대기/결재대기/승인/반려) |
|
127 |
- clsfCodeList: [], // 직급 코드 (사원/주임/대리) |
|
128 |
- rspofcCodeList: [], // 직책 코드 (팀장) |
|
129 |
- }; |
|
130 |
- |
|
131 |
- // 휴가 종류 - depth 2 조회 |
|
132 |
- const vcatnKndCodes = await Vue.config.globalProperties.$findChildCodes('sanctn_mby_vcatn'); |
|
133 |
- for (const code of vcatnKndCodes) { |
|
134 |
- const childCodes = await Vue.config.globalProperties.$findChildCodes(code.code); |
|
135 |
- codeGroups.vcatnKndCodeList.push(...childCodes); |
|
136 |
- } |
|
137 |
- |
|
138 |
- // 출장 종류 - depth 1 조회 |
|
139 |
- const bsrpCodes = await Vue.config.globalProperties.$findChildCodes('sanctn_mby_bsrp'); |
|
140 |
- codeGroups.bsrpCodeList.push(...bsrpCodes); |
|
141 |
- |
|
142 |
- // 결재 구분 - depth 1 조회 |
|
143 |
- const sanctnCodes = await Vue.config.globalProperties.$findChildCodes('sanctn_code'); |
|
144 |
- codeGroups.sanctnCodeList.push(...sanctnCodes); |
|
145 |
- |
|
146 |
- // 상태 코드 - depth 1 조회 |
|
147 |
- const confmCodes = await Vue.config.globalProperties.$findChildCodes('confm_code'); |
|
148 |
- codeGroups.confmCodeList.push(...confmCodes); |
|
149 |
- |
|
150 |
- // 직급 코드 - depth 1 조회 |
|
151 |
- const clsfCodes = await Vue.config.globalProperties.$findChildCodes('clsf_code'); |
|
152 |
- codeGroups.clsfCodeList.push(...clsfCodes); |
|
153 |
- |
|
154 |
- // 직책 코드 - depth 1 조회 |
|
155 |
- const rspofcCodes = await Vue.config.globalProperties.$findChildCodes('rspofc_code'); |
|
156 |
- codeGroups.rspofcCodeList.push(...rspofcCodes); |
|
157 |
- |
|
158 |
- return codeGroups; |
|
159 |
- }; |
|
160 | 119 |
}, |
161 | 120 |
}(파일 끝에 줄바꿈 문자 없음) |
--- client/views/component/Sanctn/SanctnFormList.vue
+++ client/views/component/Sanctn/SanctnFormList.vue
... | ... | @@ -1,29 +1,30 @@ |
1 | 1 |
<template> |
2 | 2 |
<div> |
3 | 3 |
<div v-for="(lists, idx) of lists" :key="lists.sanctnId || idx" class="draggable-item-wrapper"> |
4 |
- <div class="drop-zone" @dragover.prevent="handleDragEnter($event, idx)" @dragenter.prevent="handleDragEnter($event, idx)" @dragleave="handleDragLeave($event, idx)" @drop.prevent="handleDrop($event, idx)" :class="{ |
|
5 |
- 'drop-active': dropTarget === idx, |
|
6 |
- 'drop-visible': draggedIndex !== null && shouldShowDropZone(idx), |
|
7 |
- 'drop-hidden': draggedIndex !== null && !shouldShowDropZone(idx) |
|
4 |
+ <div class="drop-zone" @dragover.prevent="handleDragOver($event, idx)" @dragenter.prevent="handleDragEnter($event, idx)" @dragleave="handleDragLeave($event, idx)" @drop.prevent="handleDrop($event, idx)" :class="{ |
|
5 |
+ 'drop-active': currentDropTarget === idx, |
|
6 |
+ 'drop-visible': isDragging && shouldShowDropZone(idx), |
|
7 |
+ 'drop-hidden': isDragging && !shouldShowDropZone(idx) |
|
8 | 8 |
}"> |
9 | 9 |
<div class="drop-indicator">여기에 놓기</div> |
10 | 10 |
</div> |
11 |
- <div class="d-flex addapproval draggable-item" draggable="true" @dragstart="handleDragStart(idx, $event)" @dragend="handleDragEnd" :class="{ 'being-dragged': draggedIndex === idx }"> |
|
11 |
+ <div class="d-flex addapproval draggable-item" draggable="true" @dragstart="handleDragStart(idx, $event)" @dragend="handleDragEnd" :class="{ 'being-dragged': currentDraggedIndex === idx }"> |
|
12 | 12 |
<select class="form-select" v-model="lists.sanctnSe" style="width: 110px;" @mousedown.stop> |
13 |
- <option v-for="(item, idx) of cmmnCodes.sanctnCodeList" :key="idx" :value="item.code"> {{ item.codeNm }} </option> |
|
13 |
+ <option v-for="(item, idx) of approvalCodes" :key="idx" :value="item.code"> {{ item.codeNm }} </option> |
|
14 | 14 |
</select> |
15 | 15 |
<div class="d-flex align-items-center border-x"> |
16 | 16 |
<p>{{ lists.confmerNm }} {{ lists.clsfNm }} ({{ lists.sanctnOrdr }})</p> |
17 |
- <button type="button" @click="$emit('delSanctn', idx)" @mousedown.stop> |
|
17 |
+ <button type="button" @click="removeApproval(idx)" @mousedown.stop> |
|
18 | 18 |
<CloseOutlined /> |
19 | 19 |
</button> |
20 | 20 |
</div> |
21 | 21 |
</div> |
22 | 22 |
</div> |
23 |
- <div class="drop-zone" @dragover.prevent="handleDragEnter($event, lists.length)" @dragenter.prevent="handleDragEnter($event, lists.length)" @dragleave="handleDragLeave($event, lists.length)" @drop.prevent="handleDrop($event, lists.length)" :class="{ |
|
24 |
- 'drop-active': dropTarget === lists.length, |
|
25 |
- 'drop-visible': draggedIndex !== null && shouldShowLastDropZone(), |
|
26 |
- 'drop-hidden': draggedIndex !== null && !shouldShowLastDropZone() |
|
23 |
+ <!-- 마지막 드롭존 --> |
|
24 |
+ <div class="drop-zone" @dragover.prevent="handleDragOver($event, lists.length)" @dragenter.prevent="handleDragEnter($event, lists.length)" @dragleave="handleDragLeave($event, lists.length)" @drop.prevent="handleDrop($event, lists.length)" :class="{ |
|
25 |
+ 'drop-active': currentDropTarget === lists.length, |
|
26 |
+ 'drop-visible': isDragging && shouldShowLastDropZone(), |
|
27 |
+ 'drop-hidden': isDragging && !shouldShowLastDropZone() |
|
27 | 28 |
}"> |
28 | 29 |
<div class="drop-indicator">여기에 놓기</div> |
29 | 30 |
</div> |
... | ... | @@ -35,7 +36,9 @@ |
35 | 36 |
export default { |
36 | 37 |
name: 'SanctnList', |
37 | 38 |
|
38 |
- components: { CloseOutlined }, |
|
39 |
+ components: { |
|
40 |
+ CloseOutlined |
|
41 |
+ }, |
|
39 | 42 |
|
40 | 43 |
props: { |
41 | 44 |
lists: { |
... | ... | @@ -44,80 +47,140 @@ |
44 | 47 |
} |
45 | 48 |
}, |
46 | 49 |
|
50 |
+ emits: ['update:lists', 'delSanctn'], |
|
51 |
+ |
|
47 | 52 |
data() { |
48 | 53 |
return { |
49 |
- cmmnCodes: {}, // 결재 코드 목록 |
|
50 |
- draggedIndex: null, |
|
51 |
- dropTarget: null, |
|
54 |
+ approvalCodes: [], // 결재 코드 목록 |
|
55 |
+ currentDraggedIndex: null, |
|
56 |
+ currentDropTarget: null, |
|
52 | 57 |
} |
58 |
+ }, |
|
59 |
+ |
|
60 |
+ computed: { |
|
61 |
+ // 드래그 중인지 여부 |
|
62 |
+ isDragging() { |
|
63 |
+ return this.currentDraggedIndex !== null; |
|
64 |
+ }, |
|
53 | 65 |
}, |
54 | 66 |
|
55 | 67 |
async created() { |
56 |
- this.cmmnCodes = await this.$defaultCodes(); // 코드 목록 초기화 |
|
68 |
+ this.loadApprovalCodes(); // 코드 목록 초기화 |
|
57 | 69 |
}, |
58 | 70 |
|
59 | 71 |
methods: { |
60 |
- shouldShowDropZone(index) { |
|
61 |
- if (this.draggedIndex === null) return true; |
|
62 |
- if (index === this.draggedIndex || index === this.draggedIndex + 1) return false; |
|
63 |
- return true; |
|
72 |
+ // 결재 코드 목록 로드 |
|
73 |
+ async loadApprovalCodes() { |
|
74 |
+ try { |
|
75 |
+ this.approvalCodes = await this.$findChildCodes('sanctn_code'); |
|
76 |
+ } catch (error) { |
|
77 |
+ console.error('결재 코드 로드 실패:', error); |
|
78 |
+ this.approvalCodes = []; |
|
79 |
+ } |
|
64 | 80 |
}, |
65 | 81 |
|
82 |
+ // 드롭존 표시 여부 계산 |
|
83 |
+ shouldShowDropZone(targetIndex) { |
|
84 |
+ if (!this.isDragging) return true; |
|
85 |
+ |
|
86 |
+ // 현재 드래그 중인 아이템의 앞뒤 드롭존은 숨김 |
|
87 |
+ const adjacentIndexes = [this.currentDraggedIndex, this.currentDraggedIndex + 1]; |
|
88 |
+ return !adjacentIndexes.includes(targetIndex); |
|
89 |
+ }, |
|
90 |
+ |
|
91 |
+ // 마지막 드롭존 표시 여부 계산 |
|
66 | 92 |
shouldShowLastDropZone() { |
67 |
- if (this.draggedIndex === null) return true; |
|
68 |
- return this.draggedIndex !== this.lists.length - 1; |
|
93 |
+ if (!this.isDragging) return true; |
|
94 |
+ |
|
95 |
+ // 마지막 아이템을 드래그 중이면 마지막 드롭존 숨김 |
|
96 |
+ return this.currentDraggedIndex !== this.lists.length - 1; |
|
69 | 97 |
}, |
70 | 98 |
|
71 |
- handleDragStart(index, event) { |
|
72 |
- this.draggedIndex = index; |
|
99 |
+ // 드래그 시작 |
|
100 |
+ handleDragStart(itemIndex, event) { |
|
101 |
+ this.currentDraggedIndex = itemIndex; |
|
73 | 102 |
event.dataTransfer.effectAllowed = 'move'; |
74 |
- event.dataTransfer.setData('text/plain', index.toString()); |
|
103 |
+ event.dataTransfer.setData('text/plain', itemIndex.toString()); |
|
75 | 104 |
}, |
76 | 105 |
|
106 |
+ // 드래그 오버 (드롭 허용) |
|
107 |
+ handleDragOver(event, dropIndex) { |
|
108 |
+ event.preventDefault(); |
|
109 |
+ event.dataTransfer.dropEffect = 'move'; |
|
110 |
+ }, |
|
111 |
+ |
|
112 |
+ // 드래그 진입 |
|
77 | 113 |
handleDragEnter(event, dropIndex) { |
78 |
- if (this.draggedIndex !== null) { |
|
79 |
- this.dropTarget = dropIndex; |
|
114 |
+ if (this.isDragging) { |
|
115 |
+ this.currentDropTarget = dropIndex; |
|
80 | 116 |
} |
81 | 117 |
}, |
82 | 118 |
|
119 |
+ // 드래그 떠남 |
|
83 | 120 |
handleDragLeave(event, dropIndex) { |
84 |
- const rect = event.currentTarget.getBoundingClientRect(); |
|
85 |
- const x = event.clientX; |
|
86 |
- const y = event.clientY; |
|
121 |
+ const dropZone = event.currentTarget; |
|
122 |
+ const rect = dropZone.getBoundingClientRect(); |
|
123 |
+ const { clientX: x, clientY: y } = event; |
|
87 | 124 |
|
88 |
- if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) { |
|
89 |
- if (this.dropTarget === dropIndex) { |
|
90 |
- this.dropTarget = null; |
|
91 |
- } |
|
125 |
+ // 드롭존 영역을 완전히 벗어났는지 확인 |
|
126 |
+ const isOutside = x < rect.left || x > rect.right || y < rect.top || y > rect.bottom; |
|
127 |
+ |
|
128 |
+ if (isOutside && this.currentDropTarget === dropIndex) { |
|
129 |
+ this.currentDropTarget = null; |
|
92 | 130 |
} |
93 | 131 |
}, |
94 | 132 |
|
133 |
+ // 드롭 처리 |
|
95 | 134 |
handleDrop(event, dropIndex) { |
96 |
- if (this.draggedIndex !== null && this.draggedIndex !== dropIndex) { |
|
97 |
- let finalDropIndex = dropIndex; |
|
98 |
- |
|
99 |
- if (this.draggedIndex < dropIndex) { |
|
100 |
- finalDropIndex = dropIndex - 1; |
|
101 |
- } |
|
102 |
- |
|
103 |
- const newSanctns = [...this.lists]; |
|
104 |
- const draggedItem = newSanctns.splice(this.draggedIndex, 1)[0]; |
|
105 |
- newSanctns.splice(finalDropIndex, 0, draggedItem); |
|
106 |
- |
|
107 |
- newSanctns.forEach((item, index) => { |
|
108 |
- item.sanctnOrdr = index + 1; |
|
109 |
- }); |
|
110 |
- |
|
111 |
- this.$emit('update:lists', newSanctns); |
|
135 |
+ if (!this.isDragging || this.currentDraggedIndex === dropIndex) { |
|
136 |
+ this.resetDragState(); |
|
137 |
+ return; |
|
112 | 138 |
} |
113 | 139 |
|
114 |
- this.dropTarget = null; |
|
140 |
+ this.reorderApprovalList(dropIndex); |
|
141 |
+ this.resetDragState(); |
|
115 | 142 |
}, |
116 | 143 |
|
144 |
+ // 드래그 종료 |
|
117 | 145 |
handleDragEnd() { |
118 |
- this.draggedIndex = null; |
|
119 |
- this.dropTarget = null; |
|
120 |
- } |
|
146 |
+ this.resetDragState(); |
|
147 |
+ }, |
|
148 |
+ |
|
149 |
+ // 결재 목록 재정렬 |
|
150 |
+ reorderApprovalList(dropIndex) { |
|
151 |
+ let finalDropIndex = dropIndex; |
|
152 |
+ |
|
153 |
+ // 드래그한 아이템이 드롭 위치보다 앞에 있으면 인덱스 조정 |
|
154 |
+ if (this.currentDraggedIndex < dropIndex) { |
|
155 |
+ finalDropIndex = dropIndex - 1; |
|
156 |
+ } |
|
157 |
+ |
|
158 |
+ const reorderedList = [...this.lists]; |
|
159 |
+ const [draggedItem] = reorderedList.splice(this.currentDraggedIndex, 1); |
|
160 |
+ reorderedList.splice(finalDropIndex, 0, draggedItem); |
|
161 |
+ |
|
162 |
+ // 결재 순서 재설정 |
|
163 |
+ this.updateApprovalOrder(reorderedList); |
|
164 |
+ this.$emit('update:lists', reorderedList); |
|
165 |
+ }, |
|
166 |
+ |
|
167 |
+ // 결재 순서 업데이트 |
|
168 |
+ updateApprovalOrder(approvalList) { |
|
169 |
+ approvalList.forEach((item, index) => { |
|
170 |
+ item.sanctnOrdr = index + 1; |
|
171 |
+ }); |
|
172 |
+ }, |
|
173 |
+ |
|
174 |
+ // 결재자 삭제 |
|
175 |
+ removeApproval(itemIndex) { |
|
176 |
+ this.$emit('delSanctn', itemIndex); |
|
177 |
+ }, |
|
178 |
+ |
|
179 |
+ // 드래그 상태 초기화 |
|
180 |
+ resetDragState() { |
|
181 |
+ this.currentDraggedIndex = null; |
|
182 |
+ this.currentDropTarget = null; |
|
183 |
+ }, |
|
121 | 184 |
} |
122 | 185 |
}; |
123 | 186 |
</script> |
... | ... | @@ -132,11 +195,17 @@ |
132 | 195 |
background: white; |
133 | 196 |
border-radius: 8px; |
134 | 197 |
padding: 8px; |
198 |
+ cursor: grab; |
|
199 |
+} |
|
200 |
+ |
|
201 |
+.draggable-item:active { |
|
202 |
+ cursor: grabbing; |
|
135 | 203 |
} |
136 | 204 |
|
137 | 205 |
.being-dragged { |
138 | 206 |
opacity: 0.5; |
139 | 207 |
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); |
208 |
+ transform: rotate(2deg); |
|
140 | 209 |
} |
141 | 210 |
|
142 | 211 |
.drop-zone { |
... | ... | @@ -177,6 +246,7 @@ |
177 | 246 |
.drop-zone.drop-active { |
178 | 247 |
border-color: #007bff !important; |
179 | 248 |
background-color: #e3f2fd; |
249 |
+ box-shadow: 0 0 10px rgba(0, 123, 255, 0.3); |
|
180 | 250 |
} |
181 | 251 |
|
182 | 252 |
.drop-indicator { |
... | ... | @@ -184,5 +254,14 @@ |
184 | 254 |
font-weight: bold; |
185 | 255 |
font-size: inherit; |
186 | 256 |
pointer-events: none; |
257 |
+ user-select: none; |
|
258 |
+} |
|
259 |
+ |
|
260 |
+.addapproval { |
|
261 |
+ margin-bottom: 8px; |
|
262 |
+} |
|
263 |
+ |
|
264 |
+.addapproval:last-child { |
|
265 |
+ margin-bottom: 0; |
|
187 | 266 |
} |
188 | 267 |
</style>(파일 끝에 줄바꿈 문자 없음) |
--- client/views/pages/AppRouter.js
+++ client/views/pages/AppRouter.js
... | ... | @@ -9,16 +9,18 @@ |
9 | 9 |
import MyApprovalRequestListComp from '../pages/Manager/sanctn/MyApprovalRequestList.vue'; |
10 | 10 |
import PendingApprovalListComp from '../pages/Manager/sanctn/PendingApprovalList.vue'; |
11 | 11 |
|
12 |
-//근태관리 |
|
12 |
+// 근태관리 |
|
13 | 13 |
import myAttendance from '../pages/Manager/attendance/myAttendance.vue'; |
14 | 14 |
import buseoAttendance from '../pages/Manager/attendance/buseoAttendance.vue'; |
15 | 15 |
import AttendanceDetail from '../pages/Manager/attendance/AttendanceDetail.vue'; |
16 |
-import hyugaStatue from '../pages/Manager/attendance/hyugaStatue.vue'; |
|
17 |
-import HyugaInsert from '../pages/Manager/attendance/HyugaInsert.vue'; |
|
18 |
-import HyugaDetail from '../pages/Manager/attendance/HyugaDetail.vue'; |
|
16 |
+// 근태관리 - 휴가 |
|
17 |
+import VcatnListComp from '../pages/Manager/attendance/VcatnList.vue'; |
|
18 |
+import VcatnViewComp from './Manager/attendance/VcatnView.vue'; |
|
19 |
+import VcatnInsertComp from '../pages/Manager/attendance/VcatnInsert.vue'; |
|
20 |
+// 근태관리 - 출장 |
|
19 | 21 |
import BsrpListComp from '../pages/Manager/attendance/BsrpList.vue'; |
20 |
-import BsrpInsertComp from '../pages/Manager/attendance/BsrpInsert.vue'; |
|
21 | 22 |
import BsrpViewComp from './Manager/attendance/BsrpView.vue'; |
23 |
+import BsrpInsertComp from '../pages/Manager/attendance/BsrpInsert.vue'; |
|
22 | 24 |
import BsrpRportInsertComp from '../pages/Manager/attendance/BsrpRportInsert.vue'; |
23 | 25 |
|
24 | 26 |
//업무관리 |
... | ... | @@ -78,10 +80,10 @@ |
78 | 80 |
{ path: 'myAttendance.page', name: 'myAttendance', component: myAttendance }, |
79 | 81 |
{ path: 'buseoAttendance.page', name: 'buseoAttendance', component: buseoAttendance }, |
80 | 82 |
{ path: 'AttendanceDetail.page', name: 'AttendanceDetail', component: AttendanceDetail }, |
81 |
- { path: 'hyugaStatue.page', name: 'hyugaStatue', component: hyugaStatue }, |
|
82 |
- { path: 'HyugaDetail.page', name: 'HyugaDetail', component: HyugaDetail }, |
|
83 |
- { path: 'hyugaInsert.page', name: 'hyugaInsert', component: HyugaInsert }, |
|
84 | 83 |
|
84 |
+ { path: 'VcatnList.page', name: 'VcatnListPage', component: VcatnListComp }, |
|
85 |
+ { path: 'VcatnView.page', name: 'VcatnViewPage', component: VcatnViewComp }, |
|
86 |
+ { path: 'VcatnInsert.page', name: 'VcatnInsertPage', component: VcatnInsertComp }, |
|
85 | 87 |
{ path: 'BsrpList.page', name: 'BsrpListPage', component: BsrpListComp }, |
86 | 88 |
{ path: 'BsrpView.page', name: 'BsrpViewPage', component: BsrpViewComp }, |
87 | 89 |
{ path: 'BsrpInsert.page', name: 'BsrpInsertPage', component: BsrpInsertComp }, |
--- client/views/pages/Manager/attendance/HyugaDetail.vue
... | ... | @@ -1,207 +0,0 @@ |
1 | -<template> | |
2 | - <div class="card"> | |
3 | - <div class="card-body"> | |
4 | - <h2 class="card-title">휴가 현황</h2> | |
5 | - <div class="form-card"> | |
6 | - <h1>휴가신청서</h1> | |
7 | - <SanctnViewList v-if="detailData.sanctnList.length > 0" :sanctns="detailData.sanctnList" /> | |
8 | - <form class="row g-3 needs-validation detail" novalidate> | |
9 | - <div class="col-12"> | |
10 | - <div class="col-12 border-x"> | |
11 | - <label for="vcatnType" class="form-label">유형</label> | |
12 | - <p>{{ detailData.vcatnKndNm }}</p> | |
13 | - </div> | |
14 | - <div class="col-12 border-x"> | |
15 | - <label for="applicant" class="form-label">신청자</label> | |
16 | - <p>{{ detailData.userNm }}</p> | |
17 | - </div> | |
18 | - </div> | |
19 | - <div class="col-12"> | |
20 | - <div class="col-12 border-x"> | |
21 | - <label for="department" class="form-label">부서</label> | |
22 | - <p>{{ detailData.deptNm }}</p> | |
23 | - </div> | |
24 | - <div class="col-12 border-x"> | |
25 | - <label for="position" class="form-label">직급</label> | |
26 | - <p>{{ detailData.clsfNm }}</p> | |
27 | - </div> | |
28 | - </div> | |
29 | - <div class="col-12"> | |
30 | - <label for="period" class="form-label">기간</label> | |
31 | - <p>{{ $formattedDates(detailData) }}</p> | |
32 | - </div> | |
33 | - <div class="col-12 hyuga"> | |
34 | - <label for="details" class="form-label">세부사항</label> | |
35 | - <ViewerComponent :content="detailData.detailCn" /> | |
36 | - </div> | |
37 | - <div class="col-12"> | |
38 | - <label for="requestDate" class="form-label">신청일</label> | |
39 | - <p>{{ detailData.rgsde }}</p> | |
40 | - </div> | |
41 | - <div class="col-12"> | |
42 | - <label for="status" class="form-label">상태</label> | |
43 | - <p>{{ detailData.confmAtNm }}</p> | |
44 | - </div> | |
45 | - <div v-if="detailData.confmAt === 'R'" class="col-12 border-x return"> | |
46 | - <label for="rejectReason" class="form-label">반려사유</label> | |
47 | - <template v-for="(item, idx) of detailData.sanctnList" :key="idx"> | |
48 | - <p v-if="item.confmAt === 'R'">{{ item.returnResn }}</p> | |
49 | - </template> | |
50 | - </div> | |
51 | - </form> | |
52 | - </div> | |
53 | - <div class="buttons"> | |
54 | - <button v-if="detailData.confmAt === 'W' || detailData.confmAt === 'R'" class="btn sm btn-red" type="button" @click="deleteData">신청취소</button> | |
55 | - <button v-if="detailData.confmAt === 'W'" class="btn sm secondary" type="button" @click="fnMoveTo('edit', pageId)">수정</button> | |
56 | - <button v-if="detailData.confmAt === 'R'" class="btn sm secondary" type="button" @click="fnMoveTo('edit', pageId)">재신청</button> | |
57 | - <button class="btn sm tertiary" type="button" @click="fnMoveTo('list')">목록</button> | |
58 | - </div> | |
59 | - <ReturnPopup v-if="showPopup" @close="showPopup = false" /> | |
60 | - </div> | |
61 | - </div> | |
62 | -</template> | |
63 | -<script> | |
64 | -import ReturnPopup from '../../../component/Popup/ReturnPopup.vue'; | |
65 | -import ViewerComponent from '../../../component/editor/ViewerComponent.vue'; | |
66 | -import SanctnViewList from '../../../component/Sanctn/SanctnViewList.vue'; | |
67 | -// API | |
68 | -import { findVcatnProc, deleteVcatnProc } from '../../../../resources/api/vcatn'; | |
69 | - | |
70 | -export default { | |
71 | - components: { | |
72 | - ReturnPopup, ViewerComponent, | |
73 | - SanctnViewList, | |
74 | - }, | |
75 | - | |
76 | - data() { | |
77 | - return { | |
78 | - require: "/client/resources/img/require.png", | |
79 | - | |
80 | - pageId: null, | |
81 | - showPopup: false, | |
82 | - isRegister: false, | |
83 | - | |
84 | - detailData: { | |
85 | - vcatnId: null, | |
86 | - userId: null, | |
87 | - userNm: null, | |
88 | - vcatnKnd: null, | |
89 | - vcatnKndNm: null, | |
90 | - deptId: null, | |
91 | - deptNm: null, | |
92 | - clsf: null, | |
93 | - clsfNm: null, | |
94 | - bgnde: null, | |
95 | - beginHour: null, | |
96 | - beginMnt: null, | |
97 | - endde: null, | |
98 | - endHour: null, | |
99 | - endMnt: null, | |
100 | - detailCn: null, | |
101 | - confmAt: null, | |
102 | - confmAtNm: null, | |
103 | - rgsde: null, | |
104 | - register: null, | |
105 | - updde: null, | |
106 | - updusr: null, | |
107 | - sanctnList: [] | |
108 | - }, | |
109 | - }; | |
110 | - }, | |
111 | - | |
112 | - computed: {}, | |
113 | - | |
114 | - async created() { | |
115 | - this.pageId = this.$route.query.id; | |
116 | - if (this.$isEmpty(this.pageId)) { | |
117 | - alert("게시물이 존재하지 않습니다."); | |
118 | - this.fnMoveTo('list'); | |
119 | - } | |
120 | - }, | |
121 | - | |
122 | - mounted() { | |
123 | - this.findData(); // 상세 조회 | |
124 | - }, | |
125 | - | |
126 | - methods: { | |
127 | - // 상세 조회 | |
128 | - async findData() { | |
129 | - try { | |
130 | - const response = await findVcatnProc(this.pageId); | |
131 | - const result = response.data.data; | |
132 | - | |
133 | - this.detailData = result.vo; | |
134 | - this.isRegister = this.$registerChk(result.vo.register); | |
135 | - } catch (error) { | |
136 | - if (error.response) { | |
137 | - alert(error.response.data.message); | |
138 | - } else { | |
139 | - alert("에러가 발생했습니다."); | |
140 | - } | |
141 | - console.error(error.message); | |
142 | - this.fnMoveTo('list'); | |
143 | - } | |
144 | - }, | |
145 | - | |
146 | - // 일수 계산 | |
147 | - calculateDayCount() { | |
148 | - if (!this.detailData.bgnde || !this.detailData.endde) return 0; | |
149 | - | |
150 | - let dayCnt = 1; // 기본값 | |
151 | - | |
152 | - // 반차인 경우 | |
153 | - if (this.detailData.vcatnKnd === 'MORNING_HALF' || this.detailData.vcatnKnd === 'AFTERNOON_HALF') { | |
154 | - dayCnt = 0.5; | |
155 | - } | |
156 | - | |
157 | - const startDate = new Date(this.detailData.bgnde); | |
158 | - const endDate = new Date(this.detailData.endde); | |
159 | - const timeDiff = endDate.getTime() - startDate.getTime(); | |
160 | - const dayDiff = Math.floor(timeDiff / (1000 * 60 * 60 * 24)) + 1; | |
161 | - | |
162 | - return dayCnt * Math.max(0, dayDiff); | |
163 | - }, | |
164 | - | |
165 | - // 삭제 | |
166 | - async deleteData() { | |
167 | - const isCheck = confirm("삭제하시겠습니까?"); | |
168 | - if (!isCheck) { | |
169 | - return; | |
170 | - } | |
171 | - | |
172 | - try { | |
173 | - const response = await deleteVcatnProc(this.pageId); | |
174 | - | |
175 | - this.fnMoveTo('list'); | |
176 | - } catch (error) { | |
177 | - if (error.response) { | |
178 | - alert(error.response.data.message); | |
179 | - } else { | |
180 | - alert("에러가 발생했습니다."); | |
181 | - } | |
182 | - console.error(error.message); | |
183 | - this.fnMoveTo('list'); | |
184 | - } | |
185 | - }, | |
186 | - | |
187 | - // 페이지 이동 | |
188 | - fnMoveTo(type, id) { | |
189 | - const routes = { | |
190 | - 'list': { name: 'hyugaStatue' }, | |
191 | - 'view': { name: 'HyugaDetail', query: { id } }, | |
192 | - 'edit': { name: 'hyugaInsert', query: this.$isEmpty(id) ? {} : { id } }, | |
193 | - }; | |
194 | - | |
195 | - if (routes[type]) { | |
196 | - if (!this.$isEmpty(this.pageId) && type === 'list') { | |
197 | - this.$router.push({ name: 'HyugaDetail', query: { id: this.pageId } }); | |
198 | - } | |
199 | - this.$router.push(routes[type]); | |
200 | - } else { | |
201 | - alert("올바르지 않은 경로를 요청하여 목록으로 이동합니다."); | |
202 | - this.$router.push(routes['list']); | |
203 | - } | |
204 | - }, | |
205 | - } | |
206 | -}; | |
207 | -</script>(파일 끝에 줄바꿈 문자 없음) |
--- client/views/pages/Manager/attendance/HyugaInsert.vue
... | ... | @@ -1,487 +0,0 @@ |
1 | -<template> | |
2 | - <div class="card"> | |
3 | - <div class="card-body"> | |
4 | - <h2 class="card-title">휴가 신청</h2> | |
5 | - <p class="require"><img :src="require" alt=""> 필수입력</p> | |
6 | - <div class="tbl-wrap"> | |
7 | - <table class="tbl data"> | |
8 | - <tbody> | |
9 | - <tr> | |
10 | - <th scope="row"> | |
11 | - 유형 <span class="require"><img :src="require" alt=""></span> | |
12 | - </th> | |
13 | - <td> | |
14 | - <select class="form-select sm" style="max-width: 200px;" v-model="editData.vcatnKnd" @change="fnOnchangeVcatnKnd"> | |
15 | - <option value="" disabled hidden>유형 선택</option> | |
16 | - <option v-for="(item, idx) of vcatnKnds" :key="idx" :value="item.code">{{ item.codeNm }}</option> | |
17 | - </select> | |
18 | - </td> | |
19 | - </tr> | |
20 | - <tr> | |
21 | - <th scope="row"> | |
22 | - 시작일 <span class="require"><img :src="require" alt=""></span> | |
23 | - </th> | |
24 | - <td> | |
25 | - <div class="d-flex gap-1"> | |
26 | - <input type="date" class="form-control sm" v-model="editData.bgnde" @keydown="preventKeyboard" /> | |
27 | - <input type="text" class="form-control sm" placeholder="시" style="width: 100px;" v-model="editData.beginHour" readonly /> | |
28 | - <input type="text" class="form-control sm" placeholder="분" style="width: 100px;" v-model="editData.beginMnt" readonly /> | |
29 | - </div> | |
30 | - </td> | |
31 | - </tr> | |
32 | - <tr> | |
33 | - <th scope="row"> | |
34 | - 종료일 <span class="require"><img :src="require" alt=""></span> | |
35 | - </th> | |
36 | - <td> | |
37 | - <div class="d-flex gap-1"> | |
38 | - <input type="date" class="form-control sm" v-model="editData.endde" :readonly="dayCnt === 0.5" @keydown="preventKeyboard" /> | |
39 | - <input type="text" class="form-control sm" placeholder="시" style="width: 100px;" v-model="editData.endHour" readonly /> | |
40 | - <input type="text" class="form-control sm" placeholder="분" style="width: 100px;" v-model="editData.endMnt" readonly /> | |
41 | - </div> | |
42 | - </td> | |
43 | - </tr> | |
44 | - <tr> | |
45 | - <th scope="row">사용 휴가일</th> | |
46 | - <td> | |
47 | - <input type="text" class="form-control sm" v-model="totalDays" readonly /> | |
48 | - </td> | |
49 | - </tr> | |
50 | - <tr> | |
51 | - <th scope="row"> | |
52 | - 승인자 <span class="require"><img :src="require" alt=""></span> | |
53 | - </th> | |
54 | - <td> | |
55 | - <button type="button" title="추가" @click="isOpenModal = true"> | |
56 | - <PlusCircleFilled /> | |
57 | - </button> | |
58 | - <HrPopup v-if="isOpenModal" :lists="editData.sanctnList" @onSelected="fnAddSanctn" @close="isOpenModal = false" /> | |
59 | - <div class="approval-container"> | |
60 | - <SanctnList v-model:lists="editData.sanctnList" @delSanctn="fnDelSanctn" /> | |
61 | - </div> | |
62 | - </td> | |
63 | - </tr> | |
64 | - <tr> | |
65 | - <th scope="row">세부사항</th> | |
66 | - <td> | |
67 | - <EditorComponent v-model:contents="editData.detailCn" /> | |
68 | - </td> | |
69 | - </tr> | |
70 | - </tbody> | |
71 | - </table> | |
72 | - </div> | |
73 | - | |
74 | - <div class="buttons"> | |
75 | - <button type="button" class="btn sm btn-red" @click="fnRecord">이전 승인자 불러오기</button> | |
76 | - <button type="button" class="btn sm primary" @click="fnSave">신청</button> | |
77 | - <button type="button" class="btn sm tertiary" @click="fnMoveTo('list')">취소</button> | |
78 | - </div> | |
79 | - </div> | |
80 | - </div> | |
81 | -</template> | |
82 | -<script> | |
83 | -import { PlusCircleFilled, CloseOutlined } from '@ant-design/icons-vue'; | |
84 | -import HrPopup from '../../../component/Popup/HrPopup.vue'; | |
85 | -import SanctnList from '../../../component/Sanctn/SanctnFormList.vue'; | |
86 | -import EditorComponent from '../../../component/editor/EditorComponent.vue'; | |
87 | -// API | |
88 | -import { findVcatnProc, saveVcatnProc, findLastVcatnProc, updateVcatnProc } from '../../../../resources/api/vcatn'; | |
89 | - | |
90 | -export default { | |
91 | - components: { | |
92 | - PlusCircleFilled, | |
93 | - CloseOutlined, | |
94 | - HrPopup, | |
95 | - SanctnList, | |
96 | - EditorComponent, | |
97 | - }, | |
98 | - | |
99 | - data() { | |
100 | - return { | |
101 | - require: "/client/resources/img/require.png", | |
102 | - | |
103 | - pageId: null, | |
104 | - isOpenModal: false, | |
105 | - | |
106 | - editData: { | |
107 | - vcatnId: null, | |
108 | - userId: null, | |
109 | - vcatnKnd: null, | |
110 | - deptId: null, | |
111 | - clsf: null, | |
112 | - bgnde: null, | |
113 | - beginHour: null, | |
114 | - beginMnt: null, | |
115 | - endde: null, | |
116 | - endHour: null, | |
117 | - endMnt: null, | |
118 | - detailCn: null, | |
119 | - confmAt: null, | |
120 | - rgsde: null, | |
121 | - register: null, | |
122 | - updde: null, | |
123 | - updusr: null, | |
124 | - sanctnList: [] | |
125 | - }, | |
126 | - dayCnt: 0, | |
127 | - totalDays: 0, | |
128 | - workConfig: { | |
129 | - startHour: 9, // 근무 시작 시간 | |
130 | - endHour: 18, // 근무 종료 시간 | |
131 | - lunchStart: 12, // 점심 시작 시간 | |
132 | - lunchEnd: 13, // 점심 종료 시간 | |
133 | - }, | |
134 | - | |
135 | - vcatnKnds: [], | |
136 | - halfDaySubTypes: [], | |
137 | - vcatnSubKnd: null, | |
138 | - | |
139 | - sanctnCodes: [], | |
140 | - }; | |
141 | - }, | |
142 | - | |
143 | - computed: { | |
144 | - showSubTypeSelect() { | |
145 | - return this.dayCnt === 0.5; | |
146 | - }, | |
147 | - | |
148 | - // 현재 선택된 유형이 반차 하위 유형인지 확인 | |
149 | - isHalfDaySubType() { | |
150 | - return this.halfDaySubTypes.some(item => item.code === this.editData.vcatnKnd); | |
151 | - } | |
152 | - }, | |
153 | - | |
154 | - async created() { | |
155 | - this.pageId = this.$route.query.id; | |
156 | - | |
157 | - // 휴가 유형 조회 | |
158 | - this.vcatnKnds = []; // 초기화 | |
159 | - let halfDaySubTypes = []; | |
160 | - const vcatnKndCodes = await this.$findChildCodes('sanctn_mby_vcatn'); | |
161 | - for (const code of vcatnKndCodes) { | |
162 | - const childCodes = await this.$findChildCodes(code.code); | |
163 | - for (const childCode of childCodes) { | |
164 | - if (parseFloat(childCode.codeValue) === 0.5) { | |
165 | - // 반차(0.5)인 경우 해당 유형은 제외하고 하위 코드만 추가 | |
166 | - const subTypes = await this.$findChildCodes(childCode.code); | |
167 | - this.vcatnKnds.push(...subTypes); | |
168 | - halfDaySubTypes.push(...subTypes); | |
169 | - } else { | |
170 | - // 반차가 아닌 경우 그대로 추가 | |
171 | - this.vcatnKnds.push(childCode); | |
172 | - } | |
173 | - } | |
174 | - } | |
175 | - this.halfDaySubTypes = halfDaySubTypes; | |
176 | - | |
177 | - if (this.vcatnKnds.length > 0) { | |
178 | - this.editData.vcatnKnd = this.vcatnKnds[0].code; | |
179 | - } | |
180 | - | |
181 | - // 결재 구분 | |
182 | - this.sanctnCodes = await this.$findChildCodes('sanctn_code'); | |
183 | - | |
184 | - // 상세 조회 (pageId가 있는 경우) | |
185 | - if (!this.$isEmpty(this.pageId)) { | |
186 | - await this.findData(); | |
187 | - } | |
188 | - }, | |
189 | - | |
190 | - mounted() { }, | |
191 | - | |
192 | - watch: { | |
193 | - 'editData.bgnde'(newVal, oldVal) { | |
194 | - if (newVal !== oldVal) { | |
195 | - this.changeBgnde(); | |
196 | - } | |
197 | - }, | |
198 | - 'editData.endde'(newVal, oldVal) { | |
199 | - if (newVal !== oldVal) { | |
200 | - this.validateAndCalculateDays(); | |
201 | - } | |
202 | - }, | |
203 | - }, | |
204 | - | |
205 | - methods: { | |
206 | - // 상세 조회 | |
207 | - async findData() { | |
208 | - try { | |
209 | - const response = await findVcatnProc(this.pageId); | |
210 | - const result = response.data.data; | |
211 | - | |
212 | - this.editData = result.vo; | |
213 | - this.editData.bgnde = this.editData.bgnde.split(' ')[0]; | |
214 | - this.editData.endde = this.editData.endde.split(' ')[0]; | |
215 | - | |
216 | - await this.$nextTick(); | |
217 | - await this.fnOnchangeVcatnKnd(); | |
218 | - | |
219 | - } catch (error) { | |
220 | - console.error('데이터 조회 실패:', error); | |
221 | - const message = error.response?.data?.message || "데이터를 불러오는데 실패했습니다."; | |
222 | - alert(message); | |
223 | - this.fnMoveTo('list'); | |
224 | - } | |
225 | - }, | |
226 | - | |
227 | - // 유형 변경 | |
228 | - async fnOnchangeVcatnKnd() { | |
229 | - const selectedVcatn = this.vcatnKnds.find(item => item.code === this.editData.vcatnKnd); | |
230 | - if (!selectedVcatn) { | |
231 | - console.warn('선택된 휴가 유형을 찾을 수 없습니다.'); | |
232 | - return; | |
233 | - } | |
234 | - | |
235 | - // 반차 하위 유형인지 확인 | |
236 | - if (this.isHalfDaySubType) { | |
237 | - this.dayCnt = 0.5; | |
238 | - this.setHalfDayTime(selectedVcatn); | |
239 | - this.changeBgnde(); // 반차일 경우 종료일을 시작일과 동일하게 설정 | |
240 | - } else { | |
241 | - this.dayCnt = parseFloat(selectedVcatn.codeValue); | |
242 | - | |
243 | - // 전체 근무시간으로 설정 | |
244 | - this.editData.beginHour = this.workConfig.startHour.toString().padStart(2, '0'); | |
245 | - this.editData.beginMnt = '00'; | |
246 | - this.editData.endHour = this.workConfig.endHour.toString().padStart(2, '0'); | |
247 | - this.editData.endMnt = '00'; | |
248 | - this.calculateTotalDays(); | |
249 | - } | |
250 | - }, | |
251 | - | |
252 | - // 반차 시간 설정 메서드 | |
253 | - setHalfDayTime(selectedSubType) { | |
254 | - const codeValue = selectedSubType.codeValue; | |
255 | - | |
256 | - // 전체 실제 근무시간 계산 (점심시간 제외) | |
257 | - const totalHours = this.workConfig.endHour - this.workConfig.startHour; // 전체 시간 | |
258 | - const lunchHours = this.workConfig.lunchEnd - this.workConfig.lunchStart; // 점심시간 | |
259 | - const actualWorkHours = totalHours - lunchHours; // 실제 근무시간 | |
260 | - const halfWorkHours = actualWorkHours / 2; // 반차 시간 | |
261 | - | |
262 | - this.editData.beginMnt = '00'; | |
263 | - this.editData.endMnt = '00'; | |
264 | - | |
265 | - if (codeValue === 'AM') { | |
266 | - this.editData.beginHour = this.workConfig.startHour.toString().padStart(2, '0'); | |
267 | - this.editData.endHour = this.calculateTimeWithLunch(this.workConfig.startHour, halfWorkHours, 1).toString().padStart(2, '0'); | |
268 | - } else if (codeValue === 'PM') { | |
269 | - this.editData.beginHour = this.calculateTimeWithLunch(this.workConfig.endHour, halfWorkHours, -1).toString().padStart(2, '0'); | |
270 | - this.editData.endHour = this.workConfig.endHour.toString().padStart(2, '0'); | |
271 | - } | |
272 | - }, | |
273 | - | |
274 | - // 점심시간을 고려하여 시간 계산 (방향: 1=앞으로, -1=뒤로) | |
275 | - calculateTimeWithLunch(startTime, workHours, direction = 1) { | |
276 | - let currentHour = startTime; | |
277 | - let remainingHours = workHours; | |
278 | - | |
279 | - while (remainingHours > 0) { | |
280 | - if (direction === 1) { | |
281 | - // 앞으로 계산 (종료시간 구하기) | |
282 | - if (currentHour >= this.workConfig.lunchStart && currentHour < this.workConfig.lunchEnd) { | |
283 | - currentHour++; | |
284 | - } else { | |
285 | - currentHour++; | |
286 | - remainingHours--; | |
287 | - } | |
288 | - } else { | |
289 | - // 뒤로 계산 (시작시간 구하기) | |
290 | - currentHour--; | |
291 | - if (!(currentHour >= this.workConfig.lunchStart && currentHour < this.workConfig.lunchEnd)) { | |
292 | - remainingHours--; | |
293 | - } | |
294 | - } | |
295 | - } | |
296 | - | |
297 | - return currentHour; | |
298 | - }, | |
299 | - | |
300 | - // 시작일과 종료일 통일 | |
301 | - changeBgnde() { | |
302 | - if (!this.editData.bgnde) { | |
303 | - return; | |
304 | - } | |
305 | - | |
306 | - // 반차인 경우 종료일을 시작일과 동일하게 설정 | |
307 | - if (this.dayCnt === 0.5) { | |
308 | - this.editData.endde = this.editData.bgnde; | |
309 | - } | |
310 | - | |
311 | - this.validateAndCalculateDays(); // 사용 휴가일 계산 | |
312 | - }, | |
313 | - | |
314 | - // 사용 휴가일 계산 | |
315 | - validateAndCalculateDays() { | |
316 | - if (!this.editData.bgnde || !this.editData.endde) { | |
317 | - this.totalDays = 0; | |
318 | - return; | |
319 | - } | |
320 | - | |
321 | - const startDate = new Date(this.editData.bgnde); | |
322 | - const endDate = new Date(this.editData.endde); | |
323 | - | |
324 | - // 날짜 유효성 검사 | |
325 | - if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { | |
326 | - this.totalDays = 0; | |
327 | - return; | |
328 | - } | |
329 | - | |
330 | - // 종료일이 시작일보다 이전인지 확인 (반차가 아닌 경우) | |
331 | - if (this.dayCnt !== 0.5 && endDate < startDate) { | |
332 | - alert('종료일은 시작일보다 이전일 수 없습니다.'); | |
333 | - this.editData.endde = this.editData.bgnde; | |
334 | - return; | |
335 | - } | |
336 | - | |
337 | - this.calculateTotalDays(); | |
338 | - }, | |
339 | - | |
340 | - calculateTotalDays() { | |
341 | - if (!this.editData.bgnde || !this.editData.endde) { | |
342 | - this.totalDays = 0; | |
343 | - return; | |
344 | - } | |
345 | - | |
346 | - const startDate = new Date(this.editData.bgnde); | |
347 | - const endDate = new Date(this.editData.endde); | |
348 | - | |
349 | - if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { | |
350 | - this.totalDays = 0; | |
351 | - return; | |
352 | - } | |
353 | - | |
354 | - const timeDiff = endDate.getTime() - startDate.getTime(); | |
355 | - const dayDiff = Math.floor(timeDiff / (1000 * 60 * 60 * 24)) + 1; | |
356 | - | |
357 | - // 반차의 경우 0.5일, 그 외의 경우 실제 일수 계산 | |
358 | - if (this.dayCnt === 0.5) { | |
359 | - this.totalDays = 0.5; | |
360 | - } else { | |
361 | - this.totalDays = Math.max(0, dayDiff); | |
362 | - } | |
363 | - }, | |
364 | - | |
365 | - preventKeyboard(event) { | |
366 | - if (event.key !== 'Tab') { | |
367 | - event.preventDefault(); | |
368 | - } | |
369 | - }, | |
370 | - | |
371 | - // 승인자 | |
372 | - fnAddSanctn(user) { | |
373 | - const data = { | |
374 | - sanctnId: null, // 결재 아이디 | |
375 | - confmerId: user.userId, // 승인자 아이디 | |
376 | - clsf: user.clsf, // 직급 | |
377 | - sanctnOrdr: this.editData.sanctnList.length + 1, // 결재 순서 | |
378 | - sanctnIem: this.editData.vcatnKnd, // 결재 항목 | |
379 | - sanctnMbyId: null, // 결재 주체 아이디 | |
380 | - sanctnSe: this.sanctnCodes[0].code, // 결재구분 | |
381 | - | |
382 | - confmerNm: user.userNm, // 승인자 이름 | |
383 | - clsfNm: user.clsfNm, // 직급 이름 | |
384 | - }; | |
385 | - | |
386 | - this.editData.sanctnList.push(data); | |
387 | - this.isOpenModal = false; | |
388 | - }, | |
389 | - | |
390 | - fnDelSanctn(idx) { | |
391 | - this.editData.sanctnList.splice(idx, 1); | |
392 | - this.editData.sanctnList.forEach((item, index) => { | |
393 | - item.sanctnOrdr = index + 1; | |
394 | - }); | |
395 | - }, | |
396 | - | |
397 | - // 이전 승인자 조회 | |
398 | - async fnRecord() { | |
399 | - try { | |
400 | - const response = await findLastVcatnProc(); | |
401 | - const result = response.data.data; | |
402 | - | |
403 | - if (this.$isEmpty(result.lists)) { | |
404 | - alert("휴가 기록이 존재하지 않아, 이전 승인자를 불러올 수 없습니다."); | |
405 | - return; | |
406 | - } | |
407 | - | |
408 | - this.editData.sanctnList = result.lists; | |
409 | - } catch (error) { | |
410 | - console.error('이전 승인자 조회 실패:', error); | |
411 | - const message = error.response?.data?.message || "이전 승인자를 불러오는데 실패했습니다."; | |
412 | - alert(message); | |
413 | - } | |
414 | - }, | |
415 | - | |
416 | - // 유효성 검사 | |
417 | - validateForm() { | |
418 | - if (this.$isEmpty(this.editData.vcatnKnd)) { | |
419 | - alert("유형을 선택해 주세요."); | |
420 | - return false; | |
421 | - } | |
422 | - | |
423 | - if (this.$isEmpty(this.editData.bgnde)) { | |
424 | - alert("시작일을 선택해 주세요."); | |
425 | - return false; | |
426 | - } | |
427 | - | |
428 | - if (this.$isEmpty(this.editData.endde)) { | |
429 | - alert("종료일을 선택해 주세요."); | |
430 | - return false; | |
431 | - } | |
432 | - | |
433 | - if (this.$isEmpty(this.editData.sanctnList)) { | |
434 | - alert("승인자를 선택해 주세요."); | |
435 | - return false; | |
436 | - } | |
437 | - | |
438 | - return true; | |
439 | - }, | |
440 | - | |
441 | - // 신청 | |
442 | - async fnSave() { | |
443 | - try { | |
444 | - if (!this.validateForm()) { | |
445 | - return; | |
446 | - } | |
447 | - | |
448 | - // 데이터 세팅 | |
449 | - let data = this.editData; | |
450 | - if (!this.$isEmpty(this.pageId)) { | |
451 | - data.confmAt = 'W'; | |
452 | - } | |
453 | - | |
454 | - const response = this.$isEmpty(this.pageId) ? await saveVcatnProc(data) : await updateVcatnProc(data); | |
455 | - const message = this.$isEmpty(this.pageId) ? "등록되었습니다." : "수정되었습니다."; | |
456 | - alert(message); | |
457 | - | |
458 | - this.fnMoveTo('view', response.data.data.pageId); | |
459 | - } catch (error) { | |
460 | - console.error('저장 실패:', error); | |
461 | - const message = error.response.data.message || "저장에 실패했습니다."; | |
462 | - alert(message); | |
463 | - } | |
464 | - }, | |
465 | - | |
466 | - // 페이지 이동 | |
467 | - fnMoveTo(type, id) { | |
468 | - const routes = { | |
469 | - 'list': { name: 'hyugaStatue' }, | |
470 | - 'view': { name: 'HyugaDetail', query: { id } }, | |
471 | - 'edit': { name: 'hyugaInsert', query: this.$isEmpty(id) ? {} : { id } }, | |
472 | - }; | |
473 | - | |
474 | - if (routes[type]) { | |
475 | - if (!this.$isEmpty(this.pageId) && type === 'list') { | |
476 | - this.$router.push({ name: 'HyugaDetail', query: { id: this.pageId } }); | |
477 | - return; | |
478 | - } | |
479 | - this.$router.push(routes[type]); | |
480 | - } else { | |
481 | - alert("올바르지 않은 경로를 요청하여 목록으로 이동합니다."); | |
482 | - this.$router.push(routes['list']); | |
483 | - } | |
484 | - }, | |
485 | - }, | |
486 | -}; | |
487 | -</script>(파일 끝에 줄바꿈 문자 없음) |
+++ client/views/pages/Manager/attendance/VcatnInsert.vue
... | ... | @@ -0,0 +1,543 @@ |
1 | +<template> | |
2 | + <div class="card"> | |
3 | + <div class="card-body"> | |
4 | + <h2 class="card-title">휴가 신청</h2> | |
5 | + <p class="require">* 필수입력</p> | |
6 | + <div class="tbl-wrap"> | |
7 | + <table class="tbl data"> | |
8 | + <tbody> | |
9 | + <tr> | |
10 | + <th scope="row">유형 *</th> | |
11 | + <td> | |
12 | + <select class="form-select sm" v-model="vacationInfo.vcatnKnd" @change="handleVacationTypeChange"> | |
13 | + <option value="" disabled hidden>유형 선택</option> | |
14 | + <option v-for="(item, idx) of vacationTypes" :key="idx" :value="item.code">{{ item.codeNm }}</option> | |
15 | + </select> | |
16 | + </td> | |
17 | + </tr> | |
18 | + <tr> | |
19 | + <th scope="row">시작일 *</th> | |
20 | + <td> | |
21 | + <div class="d-flex gap-1"> | |
22 | + <input type="date" class="form-control sm" v-model="vacationInfo.bgnde" @keydown="preventKeyboard" /> | |
23 | + <input type="text" class="form-control sm" placeholder="시" style="width: 100px;" v-model="vacationInfo.beginHour" readonly /> | |
24 | + <input type="text" class="form-control sm" placeholder="분" style="width: 100px;" v-model="vacationInfo.beginMnt" readonly /> | |
25 | + </div> | |
26 | + </td> | |
27 | + </tr> | |
28 | + <tr> | |
29 | + <th scope="row">종료일 *</th> | |
30 | + <td> | |
31 | + <div class="d-flex gap-1"> | |
32 | + <input type="date" class="form-control sm" v-model="vacationInfo.endde" :readonly="vacationDayCount === 0.5" @keydown="preventKeyboard" /> | |
33 | + <input type="text" class="form-control sm" placeholder="시" style="width: 100px;" v-model="vacationInfo.endHour" readonly /> | |
34 | + <input type="text" class="form-control sm" placeholder="분" style="width: 100px;" v-model="vacationInfo.endMnt" readonly /> | |
35 | + </div> | |
36 | + </td> | |
37 | + </tr> | |
38 | + <tr> | |
39 | + <th scope="row">사용 휴가일</th> | |
40 | + <td> | |
41 | + <input type="text" class="form-control sm" v-model="totalDays" readonly /> | |
42 | + </td> | |
43 | + </tr> | |
44 | + <tr> | |
45 | + <th scope="row">승인자 *</th> | |
46 | + <td> | |
47 | + <button type="button" title="추가" @click="isOpenApproverModal = true"> | |
48 | + <PlusCircleFilled /> | |
49 | + </button> | |
50 | + <HrPopup v-if="isOpenApproverModal" :selectedEmployees="vacationCnsul.sanctnList" idField="confmerId" @select="handleApproverAdd" @close="isOpenApproverModal = false" /> | |
51 | + <div class="approval-container"> | |
52 | + <SanctnList v-model:lists="vacationCnsul.sanctnList" @delSanctn="handleApproverRemove" /> | |
53 | + </div> | |
54 | + </td> | |
55 | + </tr> | |
56 | + <tr> | |
57 | + <th scope="row">세부사항</th> | |
58 | + <td> | |
59 | + <EditorComponent v-model:contents="vacationCnsul.detailCn" /> | |
60 | + </td> | |
61 | + </tr> | |
62 | + </tbody> | |
63 | + </table> | |
64 | + </div> | |
65 | + <div class="buttons"> | |
66 | + <button type="button" class="btn sm btn-red" @click="handleLoadLastApprovers">이전 승인자 불러오기</button> | |
67 | + <button type="button" class="btn sm primary" v-if="$isEmpty(pageId)" @click="handleSave">신청</button> | |
68 | + <template v-else> | |
69 | + <button type="button" class="btn sm primary" v-if="isReapplyMode" @click="handleReapply">재신청</button> | |
70 | + <button type="button" class="btn sm primary" v-else @click="handleUpdate">수정</button> | |
71 | + <button type="button" class="btn sm secondary" @click="handleCancel">취소</button> | |
72 | + </template> | |
73 | + </div> | |
74 | + </div> | |
75 | + </div> | |
76 | +</template> | |
77 | +<script> | |
78 | +import { PlusCircleFilled } from '@ant-design/icons-vue'; | |
79 | +import HrPopup from '../../../component/Popup/HrPopup.vue'; | |
80 | +import SanctnList from '../../../component/Sanctn/SanctnFormList.vue'; | |
81 | +import EditorComponent from '../../../component/editor/EditorComponent.vue'; | |
82 | +// API | |
83 | +import { findVcatnKndsProc, findVcatnProc, saveVcatnProc, findLastVcatnProc, updateVcatnProc, reapplyVcatnProc } from '../../../../resources/api/vcatn'; | |
84 | + | |
85 | +export default { | |
86 | + name: 'VacationInsert', | |
87 | + | |
88 | + components: { | |
89 | + PlusCircleFilled, | |
90 | + HrPopup, | |
91 | + SanctnList, | |
92 | + EditorComponent, | |
93 | + }, | |
94 | + | |
95 | + data() { | |
96 | + return { | |
97 | + pageId: null, | |
98 | + pageMode: null, | |
99 | + isOpenApproverModal: false, | |
100 | + vacationDayCount: 0, | |
101 | + totalDays: 0, | |
102 | + workConfig: { | |
103 | + startHour: 9, | |
104 | + endHour: 18, | |
105 | + lunchStart: 12, | |
106 | + lunchEnd: 13, | |
107 | + }, | |
108 | + vacationInfo: { | |
109 | + vcatnId: null, | |
110 | + userId: null, | |
111 | + vcatnKnd: '', | |
112 | + deptId: null, | |
113 | + clsf: null, | |
114 | + bgnde: null, | |
115 | + beginHour: null, | |
116 | + beginMnt: null, | |
117 | + endde: null, | |
118 | + endHour: null, | |
119 | + endMnt: null, | |
120 | + confmAt: null, | |
121 | + rgsde: null, | |
122 | + register: null, | |
123 | + updde: null, | |
124 | + updusr: null, | |
125 | + }, | |
126 | + vacationCnsul: { | |
127 | + detailCn: null, | |
128 | + sanctnList: [] | |
129 | + }, | |
130 | + vacationTypes: [], | |
131 | + approvalCodes: [], | |
132 | + }; | |
133 | + }, | |
134 | + | |
135 | + watch: { | |
136 | + // 시작일 변동 시 날짜 재계산 | |
137 | + 'vacationInfo.bgnde'(newVal, oldVal) { | |
138 | + if (newVal !== oldVal) { | |
139 | + this.handleStartDateChange(); | |
140 | + } | |
141 | + }, | |
142 | + // 종료일 변동 시 날짜 재계산 | |
143 | + 'vacationInfo.endde'(newVal, oldVal) { | |
144 | + if (newVal !== oldVal) { | |
145 | + this.validateAndCalculateDays(); | |
146 | + } | |
147 | + }, | |
148 | + }, | |
149 | + | |
150 | + computed: { | |
151 | + // 재신청 여부 | |
152 | + isReapplyMode() { | |
153 | + return this.pageMode === 'reapply'; | |
154 | + }, | |
155 | + }, | |
156 | + | |
157 | + async created() { | |
158 | + this.pageId = this.$route.query.id; | |
159 | + this.pageMode = this.$route.query.type; | |
160 | + | |
161 | + await this.fetchVacationTypes(); // 휴가 유형 조회 | |
162 | + this.approvalCodes = await this.$findChildCodes('sanctn_code'); // 결재 구분 조회 | |
163 | + }, | |
164 | + | |
165 | + mounted() { | |
166 | + if (!this.$isEmpty(this.pageId)) { | |
167 | + this.fetchData(); // 상세 조회 | |
168 | + } | |
169 | + }, | |
170 | + | |
171 | + methods: { | |
172 | + // 휴가 유형 조회 | |
173 | + async fetchVacationTypes() { | |
174 | + try { | |
175 | + const response = await findVcatnKndsProc(); | |
176 | + const result = response.data.data; | |
177 | + | |
178 | + this.vacationTypes = result; | |
179 | + } catch (error) { | |
180 | + this.handleError(error); | |
181 | + this.handleNavigation('list'); | |
182 | + } | |
183 | + }, | |
184 | + | |
185 | + // 휴가 정보 조회 | |
186 | + async fetchData() { | |
187 | + try { | |
188 | + const response = await findVcatnProc(this.pageId); | |
189 | + const result = response.data.data; | |
190 | + | |
191 | + this.vacationInfo = { | |
192 | + vcatnId: result.vcatnId, | |
193 | + userId: result.userId, | |
194 | + vcatnKnd: result.vcatnKnd, | |
195 | + deptId: result.deptId, | |
196 | + clsf: result.clsf, | |
197 | + bgnde: result.bgnde, | |
198 | + beginHour: result.beginHour, | |
199 | + beginMnt: result.beginMnt, | |
200 | + endde: result.endde, | |
201 | + endHour: result.endHour, | |
202 | + endMnt: result.endMnt, | |
203 | + confmAt: result.confmAt, | |
204 | + rgsde: result.rgsde, | |
205 | + register: result.register, | |
206 | + updde: result.updde, | |
207 | + updusr: result.updusr, | |
208 | + }; | |
209 | + this.vacationCnsul = { | |
210 | + detailCn: result.detailCn, | |
211 | + sanctnList: result.sanctnList | |
212 | + }; | |
213 | + } catch (error) { | |
214 | + this.handleError(error); | |
215 | + this.handleNavigation('list'); | |
216 | + } | |
217 | + }, | |
218 | + | |
219 | + // 휴가 유형 변경 핸들러 | |
220 | + handleVacationTypeChange() { | |
221 | + const selectedVacation = this.vacationTypes.find(item => item.code === this.vacationInfo.vcatnKnd); | |
222 | + if (!selectedVacation) { | |
223 | + console.warn('선택된 휴가 유형을 찾을 수 없습니다.'); | |
224 | + return; | |
225 | + } | |
226 | + | |
227 | + if (selectedVacation.codeValue != 1) { | |
228 | + this.setHalfDayTime(selectedVacation.codeValue); | |
229 | + this.handleStartDateChange(); // 반차일 경우 종료일을 시작일과 동일하게 설정 | |
230 | + } else { | |
231 | + this.vacationDayCount = parseFloat(selectedVacation.codeValue); | |
232 | + | |
233 | + // 전체 근무시간으로 설정 | |
234 | + this.vacationInfo.beginHour = this.workConfig.startHour.toString().padStart(2, '0'); | |
235 | + this.vacationInfo.beginMnt = '00'; | |
236 | + this.vacationInfo.endHour = this.workConfig.endHour.toString().padStart(2, '0'); | |
237 | + this.vacationInfo.endMnt = '00'; | |
238 | + this.calculateTotalDays(); | |
239 | + } | |
240 | + }, | |
241 | + | |
242 | + // 반차 시간 설정 | |
243 | + setHalfDayTime(codeValue) { | |
244 | + // 전체 실제 근무시간 계산 (점심시간 제외) | |
245 | + const totalHours = this.workConfig.endHour - this.workConfig.startHour; | |
246 | + const lunchHours = this.workConfig.lunchEnd - this.workConfig.lunchStart; | |
247 | + const actualWorkHours = totalHours - lunchHours; | |
248 | + const halfWorkHours = actualWorkHours / 2; | |
249 | + | |
250 | + this.vacationInfo.beginMnt = '00'; | |
251 | + this.vacationInfo.endMnt = '00'; | |
252 | + | |
253 | + if (codeValue === 'AM') { | |
254 | + this.vacationDayCount = 0.5; | |
255 | + this.vacationInfo.beginHour = this.workConfig.startHour.toString().padStart(2, '0'); | |
256 | + this.vacationInfo.endHour = this.calculateTimeWithLunch(this.workConfig.startHour, halfWorkHours, 1).toString().padStart(2, '0'); | |
257 | + } else if (codeValue === 'PM') { | |
258 | + this.vacationDayCount = 0.5; | |
259 | + this.vacationInfo.beginHour = this.calculateTimeWithLunch(this.workConfig.endHour, halfWorkHours, -1).toString().padStart(2, '0'); | |
260 | + this.vacationInfo.endHour = this.workConfig.endHour.toString().padStart(2, '0'); | |
261 | + } | |
262 | + }, | |
263 | + | |
264 | + // 점심시간을 고려하여 시간 계산 (방향: 1=앞으로, -1=뒤로) | |
265 | + calculateTimeWithLunch(startTime, workHours, direction = 1) { | |
266 | + let currentHour = startTime; | |
267 | + let remainingHours = workHours; | |
268 | + | |
269 | + while (remainingHours > 0) { | |
270 | + if (direction === 1) { | |
271 | + // 앞으로 계산 (종료시간 구하기) | |
272 | + if (currentHour >= this.workConfig.lunchStart && currentHour < this.workConfig.lunchEnd) { | |
273 | + currentHour++; | |
274 | + } else { | |
275 | + currentHour++; | |
276 | + remainingHours--; | |
277 | + } | |
278 | + } else { | |
279 | + // 뒤로 계산 (시작시간 구하기) | |
280 | + currentHour--; | |
281 | + if (!(currentHour >= this.workConfig.lunchStart && currentHour < this.workConfig.lunchEnd)) { | |
282 | + remainingHours--; | |
283 | + } | |
284 | + } | |
285 | + } | |
286 | + | |
287 | + return currentHour; | |
288 | + }, | |
289 | + | |
290 | + // 시작일 변경 핸들러 | |
291 | + handleStartDateChange() { | |
292 | + if (!this.vacationInfo.bgnde) { | |
293 | + return; | |
294 | + } | |
295 | + | |
296 | + // 반차인 경우 종료일을 시작일과 동일하게 설정 | |
297 | + if (this.vacationDayCount < 1) { | |
298 | + this.vacationInfo.endde = this.vacationInfo.bgnde; | |
299 | + } | |
300 | + | |
301 | + this.validateAndCalculateDays(); | |
302 | + }, | |
303 | + | |
304 | + // 사용 휴가일 유효성 검사 및 계산 | |
305 | + validateAndCalculateDays() { | |
306 | + if (!this.vacationInfo.bgnde || !this.vacationInfo.endde) { | |
307 | + this.totalDays = 0; | |
308 | + return; | |
309 | + } | |
310 | + | |
311 | + const startDate = new Date(this.vacationInfo.bgnde); | |
312 | + const endDate = new Date(this.vacationInfo.endde); | |
313 | + | |
314 | + // 날짜 유효성 검사 | |
315 | + if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { | |
316 | + this.totalDays = 0; | |
317 | + return; | |
318 | + } | |
319 | + | |
320 | + // 종료일이 시작일보다 이전인지 확인 (반차가 아닌 경우) | |
321 | + if (this.vacationDayCount == 1 && endDate < startDate) { | |
322 | + alert('종료일은 시작일보다 이전일 수 없습니다.'); | |
323 | + this.vacationInfo.endde = this.vacationInfo.bgnde; | |
324 | + return; | |
325 | + } | |
326 | + | |
327 | + this.calculateTotalDays(); | |
328 | + }, | |
329 | + | |
330 | + // 총 휴가일 계산 | |
331 | + calculateTotalDays() { | |
332 | + if (!this.vacationInfo.bgnde || !this.vacationInfo.endde) { | |
333 | + this.totalDays = 0; | |
334 | + return; | |
335 | + } | |
336 | + | |
337 | + const startDate = new Date(this.vacationInfo.bgnde); | |
338 | + const endDate = new Date(this.vacationInfo.endde); | |
339 | + | |
340 | + if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { | |
341 | + this.totalDays = 0; | |
342 | + return; | |
343 | + } | |
344 | + | |
345 | + const timeDiff = endDate.getTime() - startDate.getTime(); | |
346 | + const dayDiff = Math.floor(timeDiff / (1000 * 60 * 60 * 24)) + 1; | |
347 | + | |
348 | + // 반차의 경우 0.5일, 그 외의 경우 실제 일수 계산 | |
349 | + if (this.vacationDayCount === 0.5) { | |
350 | + this.totalDays = 0.5; | |
351 | + } else { | |
352 | + this.totalDays = Math.max(0, dayDiff); | |
353 | + } | |
354 | + }, | |
355 | + | |
356 | + // 키보드 입력 방지 | |
357 | + preventKeyboard(event) { | |
358 | + if (event.key !== 'Tab') { | |
359 | + event.preventDefault(); | |
360 | + } | |
361 | + }, | |
362 | + | |
363 | + // 승인자 추가 핸들러 | |
364 | + handleApproverAdd(user) { | |
365 | + const data = { | |
366 | + confmerId: user.userId, | |
367 | + confmerNm: user.userNm, | |
368 | + clsf: user.clsf, | |
369 | + clsfNm: user.clsfNm, | |
370 | + sanctnOrdr: this.vacationCnsul.sanctnList.length + 1, | |
371 | + sanctnIem: 'sanctn_mby_vcatn', | |
372 | + sanctnSe: this.approvalCodes[0].code, | |
373 | + }; | |
374 | + | |
375 | + this.vacationCnsul.sanctnList.push(data); | |
376 | + this.isOpenApproverModal = false; | |
377 | + }, | |
378 | + | |
379 | + // 승인자 삭제 핸들러 | |
380 | + handleApproverRemove(idx) { | |
381 | + this.vacationCnsul.sanctnList.splice(idx, 1); | |
382 | + this.vacationCnsul.sanctnList.forEach((item, index) => { | |
383 | + item.sanctnOrdr = index + 1; | |
384 | + }); | |
385 | + }, | |
386 | + | |
387 | + // 이전 승인자 불러오기 핸들러 | |
388 | + async handleLoadLastApprovers() { | |
389 | + try { | |
390 | + const response = await findLastVcatnProc(); | |
391 | + const result = response.data.data; | |
392 | + | |
393 | + if (this.$isEmpty(result)) { | |
394 | + alert("휴가 기록이 존재하지 않아, 이전 승인자를 불러올 수 없습니다."); | |
395 | + return; | |
396 | + } | |
397 | + | |
398 | + this.vacationCnsul.sanctnList = result; | |
399 | + } catch (error) { | |
400 | + this.handleError(error); | |
401 | + } | |
402 | + }, | |
403 | + | |
404 | + // 저장 핸들러 | |
405 | + async handleSave() { | |
406 | + try { | |
407 | + if (!this.validateForm()) { | |
408 | + return; | |
409 | + } | |
410 | + | |
411 | + const data = this.buildSendData(); | |
412 | + | |
413 | + const response = await saveVcatnProc(data); | |
414 | + alert("등록되었습니다."); | |
415 | + | |
416 | + this.handleNavigation('view', response.data.data.pageId); | |
417 | + } catch (error) { | |
418 | + this.handleError(error); | |
419 | + } | |
420 | + }, | |
421 | + | |
422 | + // 수정 핸들러 | |
423 | + async handleUpdate() { | |
424 | + try { | |
425 | + if (!this.validateForm()) { | |
426 | + return; | |
427 | + } | |
428 | + | |
429 | + const data = this.buildSendData(); | |
430 | + | |
431 | + const response = await updateVcatnProc(this.pageId, data); | |
432 | + alert("수정되었습니다."); | |
433 | + | |
434 | + this.handleNavigation('view', response.data.data.pageId); | |
435 | + } catch (error) { | |
436 | + this.handleError(error); | |
437 | + } | |
438 | + }, | |
439 | + | |
440 | + // 재신청 핸들러 | |
441 | + async handleReapply() { | |
442 | + try { | |
443 | + if (!this.validateForm()) { | |
444 | + return; | |
445 | + } | |
446 | + | |
447 | + const data = this.buildSendData(); | |
448 | + | |
449 | + const response = await reapplyVcatnProc(this.pageId, data); | |
450 | + alert("재신청되었습니다."); | |
451 | + | |
452 | + this.handleNavigation('view', response.data.data.pageId); | |
453 | + } catch (error) { | |
454 | + this.handleError(error); | |
455 | + } | |
456 | + }, | |
457 | + | |
458 | + // 취소 핸들러 | |
459 | + handleCancel() { | |
460 | + if (confirm('작성 중인 내용이 삭제됩니다. 계속하시겠습니까?')) { | |
461 | + this.handleNavigation('view', this.pageId); | |
462 | + } | |
463 | + }, | |
464 | + | |
465 | + // 페이지 이동 핸들러 | |
466 | + handleNavigation(type, id) { | |
467 | + const routeMap = { | |
468 | + 'list': { name: this.pageMode === 'sanctns' ? 'PendingApprovalListPage' : 'VcatnListPage' }, | |
469 | + 'view': { name: 'VcatnViewPage', query: { id } }, | |
470 | + 'insert': { name: 'VcatnInsertPage', query: this.$isEmpty(id) ? {} : { id } }, | |
471 | + 'reapply': { name: 'VcatnInsertPage', query: { id, type: 'reapply' } }, | |
472 | + }; | |
473 | + | |
474 | + const route = routeMap[type]; | |
475 | + if (route) { | |
476 | + this.$router.push(route); | |
477 | + } else { | |
478 | + alert("올바르지 않은 경로입니다."); | |
479 | + this.$router.push(routeMap['list']); | |
480 | + } | |
481 | + }, | |
482 | + | |
483 | + // 에러 핸들러 | |
484 | + handleError(error) { | |
485 | + const message = error.response?.data?.message || '에러가 발생했습니다.'; | |
486 | + alert(message); | |
487 | + }, | |
488 | + | |
489 | + // 입력값 전체 유효성 검사 유틸리티 | |
490 | + validateForm() { | |
491 | + if (this.$isEmpty(this.vacationInfo.vcatnKnd)) { | |
492 | + alert("유형을 선택해 주세요."); | |
493 | + return false; | |
494 | + } | |
495 | + | |
496 | + if (this.$isEmpty(this.vacationInfo.bgnde)) { | |
497 | + alert("시작일을 선택해 주세요."); | |
498 | + return false; | |
499 | + } | |
500 | + | |
501 | + if (this.$isEmpty(this.vacationInfo.endde)) { | |
502 | + alert("종료일을 선택해 주세요."); | |
503 | + return false; | |
504 | + } | |
505 | + | |
506 | + if (this.$isEmpty(this.vacationCnsul.sanctnList)) { | |
507 | + alert("승인자를 선택해 주세요."); | |
508 | + return false; | |
509 | + } | |
510 | + | |
511 | + return true; | |
512 | + }, | |
513 | + | |
514 | + // 데이터 가공 유틸리티 | |
515 | + buildSendData() { | |
516 | + const sanctnList = []; | |
517 | + for (let sanctn of this.vacationCnsul.sanctnList) { | |
518 | + sanctnList.push({ | |
519 | + confmerId: sanctn.confmerId, | |
520 | + clsf: sanctn.clsf, | |
521 | + sanctnOrdr: sanctn.sanctnOrdr, | |
522 | + sanctnIem: sanctn.sanctnIem, | |
523 | + sanctnMbyId: sanctn.sanctnMbyId, | |
524 | + sanctnSe: sanctn.sanctnSe, | |
525 | + }); | |
526 | + } | |
527 | + | |
528 | + return { | |
529 | + userId: this.vacationInfo.userId, | |
530 | + vcatnKnd: this.vacationInfo.vcatnKnd, | |
531 | + bgnde: this.vacationInfo.bgnde, | |
532 | + beginHour: this.vacationInfo.beginHour, | |
533 | + beginMnt: this.vacationInfo.beginMnt, | |
534 | + endde: this.vacationInfo.endde, | |
535 | + endHour: this.vacationInfo.endHour, | |
536 | + endMnt: this.vacationInfo.endMnt, | |
537 | + detailCn: this.vacationCnsul.detailCn, | |
538 | + sanctnList: sanctnList, | |
539 | + }; | |
540 | + }, | |
541 | + }, | |
542 | +}; | |
543 | +</script>(파일 끝에 줄바꿈 문자 없음) |
+++ client/views/pages/Manager/attendance/VcatnList.vue
... | ... | @@ -0,0 +1,237 @@ |
1 | +<template> | |
2 | + <div class="col-lg-12"> | |
3 | + <div class="card"> | |
4 | + <div class="card-body"> | |
5 | + <h2 class="card-title">휴가 현황</h2> | |
6 | + <div class="form-group"> | |
7 | + <div class="form-conts"> | |
8 | + <div class="form-conts datepicker-conts"> | |
9 | + <div class="datepicker-input"> | |
10 | + <input type="date" class="form-control datepicker cal" v-model="searchParams.bgnde" style="max-width: 200px;" @change="handleSearchChange" /> | |
11 | + <mark>~</mark> | |
12 | + <input type="date" class="form-control datepicker cal" v-model="searchParams.endde" style="max-width: 200px;" @change="handleSearchChange" /> | |
13 | + </div> | |
14 | + </div> | |
15 | + </div> | |
16 | + </div> | |
17 | + <div class="boxs"> | |
18 | + <div class="color-boxs"> | |
19 | + <div class="box" @click="handleFilterChange()"> | |
20 | + <h3>전체</h3> | |
21 | + <div> | |
22 | + <span>{{ totalDays }}</span> | |
23 | + <small>({{ carriedOverDays }})</small> | |
24 | + </div> | |
25 | + </div> | |
26 | + <div class="box" @click="handleFilterChange(usedLeaveItem.code)"> | |
27 | + <h3>사용</h3> | |
28 | + <div>{{ usedLeaveItem.userSpendCnt }}</div> | |
29 | + </div> | |
30 | + <div class="box"> | |
31 | + <h3>미사용</h3> | |
32 | + <div>{{ remainingDays }}</div> | |
33 | + </div> | |
34 | + <div class="box" v-for="(item, idx) of otherItems" :key="idx" @click="handleFilterChange(item.code)"> | |
35 | + <h3>{{ item.codeNm }}</h3> | |
36 | + <div>{{ item.userSpendCnt }}</div> | |
37 | + </div> | |
38 | + </div> | |
39 | + </div> | |
40 | + <div class="tbl-wrap"> | |
41 | + <table id="myTable" class="tbl data"> | |
42 | + <colgroup> | |
43 | + <col style="width: 15%;"> | |
44 | + <col style="width: 40%;"> | |
45 | + <col style="width: 30%;"> | |
46 | + <col style="width: 15%;"> | |
47 | + </colgroup> | |
48 | + <thead> | |
49 | + <tr> | |
50 | + <th>구분</th> | |
51 | + <th>기간</th> | |
52 | + <th>신청일</th> | |
53 | + <th>상태</th> | |
54 | + </tr> | |
55 | + </thead> | |
56 | + <tbody> | |
57 | + <template v-if="hasVacations"> | |
58 | + <tr v-for="(item, idx) in vacationList" :key="`${item.vcatnId}-${idx}`" @click="handleItemClick(item.vcatnId)"> | |
59 | + <td>{{ item.vcatnKndNm }}</td> | |
60 | + <td>{{ $formattedDates(item) }}</td> | |
61 | + <td>{{ item.rgsde }}</td> | |
62 | + <td>{{ item.confmAtNm }}</td> | |
63 | + </tr> | |
64 | + </template> | |
65 | + <tr v-else> | |
66 | + <td colspan="4">해당 기간 내 등록된 휴가가 없습니다.</td> | |
67 | + </tr> | |
68 | + </tbody> | |
69 | + </table> | |
70 | + </div> | |
71 | + <Pagination :search="searchParams" @onChange="handlePageChange" /> | |
72 | + </div> | |
73 | + </div> | |
74 | + </div> | |
75 | +</template> | |
76 | +<script> | |
77 | +// API | |
78 | +import { findVcatnSummaryProc, findVcatnsProc } from '../../../../resources/api/vcatn'; | |
79 | + | |
80 | +export default { | |
81 | + name: 'VacationList', | |
82 | + | |
83 | + data() { | |
84 | + return { | |
85 | + searchParams: { | |
86 | + bgnde: null, | |
87 | + endde: null, | |
88 | + vcatnKnd: null, | |
89 | + currentUserId: this.$store.state.userInfo.userId, | |
90 | + }, | |
91 | + vacationList: [], | |
92 | + totalDays: 0, | |
93 | + carriedOverDays: 0, | |
94 | + usedLeaveItem: {}, | |
95 | + remainingDays: 0, | |
96 | + otherItems: [], | |
97 | + }; | |
98 | + }, | |
99 | + | |
100 | + computed: { | |
101 | + // 휴가 목록 유무 | |
102 | + hasVacations() { | |
103 | + return this.vacationList.length > 0; | |
104 | + }, | |
105 | + }, | |
106 | + | |
107 | + async mounted() { | |
108 | + await this.fetchVacationSummary(); // 휴가 현황 조회 | |
109 | + await this.fetchVacationList(); // 휴가 목록 조회 | |
110 | + }, | |
111 | + | |
112 | + methods: { | |
113 | + // 휴가 현황 조회 | |
114 | + async fetchVacationSummary() { | |
115 | + try { | |
116 | + const response = await findVcatnSummaryProc(this.searchParams); | |
117 | + const result = response.data.data; | |
118 | + | |
119 | + this.searchParams.bgnde = result.userYrycVO.bgnde; | |
120 | + this.searchParams.endde = new Date().toISOString().split('T')[0]; | |
121 | + | |
122 | + this.totalDays = result.totalDays; | |
123 | + this.carriedOverDays = result.carriedOverDays; | |
124 | + this.usedLeaveItem = result.usedLeaveItem; | |
125 | + this.remainingDays = result.remainingDays; | |
126 | + this.otherItems = result.otherItems; | |
127 | + } catch (error) { | |
128 | + this.handleError(error); | |
129 | + } | |
130 | + }, | |
131 | + | |
132 | + // 휴가 목록 조회 | |
133 | + async fetchVacationList() { | |
134 | + try { | |
135 | + delete this.searchParams.vcatnKndList; | |
136 | + | |
137 | + const response = await findVcatnsProc(this.searchParams); | |
138 | + const result = response.data.data; | |
139 | + | |
140 | + this.vacationList = result.lists; | |
141 | + this.searchParams = result.search; | |
142 | + } catch (error) { | |
143 | + this.handleError(error); | |
144 | + } | |
145 | + }, | |
146 | + | |
147 | + // 검색 핸들러 (검색 시 현재 페이지를 1로 변경 후 조회) | |
148 | + async handleSearchChange() { | |
149 | + if (!this.searchParams.bgnde || !this.searchParams.endde) { | |
150 | + return; | |
151 | + } | |
152 | + | |
153 | + const startDate = new Date(this.searchParams.bgnde); | |
154 | + const endDate = new Date(this.searchParams.endde); | |
155 | + if (endDate < startDate) { | |
156 | + alert('종료일은 시작일보다 이전일 수 없습니다.'); | |
157 | + this.searchParams.endde = this.searchParams.bgnde; | |
158 | + return; | |
159 | + } | |
160 | + | |
161 | + await this.handlePageChange(1); | |
162 | + }, | |
163 | + | |
164 | + // 필터 변경 핸들러 | |
165 | + async handleFilterChange(vcatnKnd) { | |
166 | + this.searchParams.vcatnKnd = vcatnKnd; | |
167 | + await this.handlePageChange(1); | |
168 | + }, | |
169 | + | |
170 | + // 페이지 변경 핸들러 | |
171 | + async handlePageChange(currentPage) { | |
172 | + this.searchParams.currentPage = Number(currentPage); | |
173 | + await this.$nextTick(); | |
174 | + await this.fetchVacationList(); | |
175 | + }, | |
176 | + | |
177 | + // 상세 페이지 이동 핸들러 | |
178 | + handleItemClick(id) { | |
179 | + this.handleNavigation('view', id); | |
180 | + }, | |
181 | + | |
182 | + // 페이지 이동 핸들러 | |
183 | + handleNavigation(type, id) { | |
184 | + const routeMap = { | |
185 | + 'list': { name: 'VcatnListPage' }, | |
186 | + 'view': { name: 'VcatnViewPage', query: { id } }, | |
187 | + 'insert': { name: 'VcatnInsertPage', query: this.$isEmpty(id) ? {} : { id } }, | |
188 | + 'reapply': { name: 'VcatnInsertPage', query: { id, type: 'reapply' } }, | |
189 | + }; | |
190 | + | |
191 | + const route = routeMap[type]; | |
192 | + if (route) { | |
193 | + this.$router.push(route); | |
194 | + } else { | |
195 | + alert("올바르지 않은 경로입니다."); | |
196 | + this.$router.push(routeMap['list']); | |
197 | + } | |
198 | + }, | |
199 | + | |
200 | + // 에러 핸들러 | |
201 | + handleError(error) { | |
202 | + const message = error.response?.data?.message || "에러가 발생했습니다."; | |
203 | + alert(message); | |
204 | + }, | |
205 | + }, | |
206 | +}; | |
207 | +</script> | |
208 | +<style scoped> | |
209 | +tr { | |
210 | + cursor: pointer; | |
211 | +} | |
212 | + | |
213 | +/* 미사용만 커서 미적용 (필터 조회 안됨) */ | |
214 | +.box:not(:nth-child(3)) { | |
215 | + cursor: pointer; | |
216 | +} | |
217 | + | |
218 | +.box:nth-child(2) { | |
219 | + color: #1D75E1 !important; | |
220 | +} | |
221 | + | |
222 | +.box:nth-child(3) { | |
223 | + color: #E92727 !important; | |
224 | +} | |
225 | + | |
226 | +.box:nth-child(4) { | |
227 | + color: #3C97AB !important; | |
228 | +} | |
229 | + | |
230 | +.box:nth-child(5) { | |
231 | + color: #A36CD4 !important; | |
232 | +} | |
233 | + | |
234 | +.box:nth-child(6) { | |
235 | + color: #F7941C !important; | |
236 | +} | |
237 | +</style>(파일 끝에 줄바꿈 문자 없음) |
+++ client/views/pages/Manager/attendance/VcatnView.vue
... | ... | @@ -0,0 +1,257 @@ |
1 | +<template> | |
2 | + <div class="card"> | |
3 | + <div class="card-body"> | |
4 | + <h2 class="card-title">휴가 현황</h2> | |
5 | + <div class="form-card"> | |
6 | + <h1>휴가신청서</h1> | |
7 | + <SanctnViewList v-if="vacationInfo.sanctnList.length > 0" :sanctns="vacationInfo.sanctnList" /> | |
8 | + <div class="tbl-wrap"> | |
9 | + <table class="tbl data"> | |
10 | + <tbody> | |
11 | + <tr> | |
12 | + <th>유형</th> | |
13 | + <td>{{ vacationInfo.vcatnKndNm }}</td> | |
14 | + <th>신청자</th> | |
15 | + <td>{{ vacationInfo.userNm }}</td> | |
16 | + </tr> | |
17 | + <tr> | |
18 | + <th>부서</th> | |
19 | + <td>{{ vacationInfo.deptNm }}</td> | |
20 | + <th>직급</th> | |
21 | + <td>{{ vacationInfo.clsfNm }}</td> | |
22 | + </tr> | |
23 | + <tr> | |
24 | + <th>기간</th> | |
25 | + <td colspan="3">{{ $formattedDates(vacationInfo) }}</td> | |
26 | + </tr> | |
27 | + <tr> | |
28 | + <th>세부사항</th> | |
29 | + <td colspan="3"> | |
30 | + <ViewerComponent :content="vacationInfo.detailCn" /> | |
31 | + </td> | |
32 | + </tr> | |
33 | + <tr> | |
34 | + <th>신청일</th> | |
35 | + <td colspan="3">{{ vacationInfo.rgsde }}</td> | |
36 | + </tr> | |
37 | + <tr> | |
38 | + <th>상태</th> | |
39 | + <td colspan="3">{{ vacationInfo.confmAtNm }}</td> | |
40 | + </tr> | |
41 | + <tr v-if="approvalStatus === 'rejected'"> | |
42 | + <th>반려사유</th> | |
43 | + <td colspan="3">{{ getRejectionReason(vacationInfo.sanctnList) }}</td> | |
44 | + </tr> | |
45 | + <!-- 붉은 색 등 하여간 눈에 띄게 표기해야 함!! --> | |
46 | + <tr> | |
47 | + <td colspan="4">금년도 연차 소진으로 내년도 연차를 선 사용하는 휴가 신청건입니다.</td> | |
48 | + </tr> | |
49 | + </tbody> | |
50 | + </table> | |
51 | + </div> | |
52 | + </div> | |
53 | + <div class="buttons"> | |
54 | + <template v-if="isConsultationApprover"> | |
55 | + <button type="button" class="btn sm primary" @click="handleApproval('A')">승인</button> | |
56 | + <button type="button" class="btn sm btn-red" @click="showConsultationPopup = true">반려</button> | |
57 | + </template> | |
58 | + <template v-else-if="isApplicant"> | |
59 | + <template v-if="approvalStatus === 'waiting'"> | |
60 | + <button type="button" class="btn sm btn-red" @click="handleDelete">신청취소</button> | |
61 | + <button type="button" class="btn sm secondary" @click="handleNavigation('insert', pageId)">수정</button> | |
62 | + </template> | |
63 | + <template v-if="approvalStatus === 'rejected'"> | |
64 | + <button type="button" class="btn sm secondary" @click="handleNavigation('reapply', pageId)">재신청</button> | |
65 | + </template> | |
66 | + </template> | |
67 | + <button type="button" class="btn sm tertiary" @click="handleNavigation('list')">목록</button> | |
68 | + </div> | |
69 | + </div> | |
70 | + </div> | |
71 | + <ReturnPopup v-if="showConsultationPopup" @close="showConsultationPopup = false" @confirm="handleRejection" /> | |
72 | +</template> | |
73 | +<script> | |
74 | +import ReturnPopup from '../../../component/Popup/ReturnPopup.vue'; | |
75 | +import ViewerComponent from '../../../component/editor/ViewerComponent.vue'; | |
76 | +import SanctnViewList from '../../../component/Sanctn/SanctnViewList.vue'; | |
77 | +// API | |
78 | +import { findVcatnProc, deleteVcatnProc } from '../../../../resources/api/vcatn'; | |
79 | +import { updateConfmAtProc } from '../../../../resources/api/sanctns'; | |
80 | + | |
81 | +export default { | |
82 | + name: 'VacationView', | |
83 | + | |
84 | + components: { | |
85 | + ReturnPopup, | |
86 | + ViewerComponent, | |
87 | + SanctnViewList, | |
88 | + }, | |
89 | + | |
90 | + data() { | |
91 | + return { | |
92 | + pageId: null, | |
93 | + pageMode: null, | |
94 | + showConsultationPopup: false, | |
95 | + vacationInfo: { | |
96 | + userId: null, | |
97 | + userNm: null, | |
98 | + vcatnKndNm: null, | |
99 | + deptNm: null, | |
100 | + clsfNm: null, | |
101 | + bgnde: null, | |
102 | + beginHour: null, | |
103 | + beginMnt: null, | |
104 | + endde: null, | |
105 | + endHour: null, | |
106 | + endMnt: null, | |
107 | + detailCn: null, | |
108 | + confmAtNm: null, | |
109 | + rgsde: null, | |
110 | + sanctnList: [] | |
111 | + }, | |
112 | + returnResn: null, | |
113 | + }; | |
114 | + }, | |
115 | + | |
116 | + computed: { | |
117 | + // 결재 상태 | |
118 | + approvalStatus() { | |
119 | + const sanctnList = this.vacationInfo.sanctnList; | |
120 | + if (sanctnList.length === 0) return 'none'; | |
121 | + | |
122 | + // 하나라도 반려된 경우 > 반려 | |
123 | + if (sanctnList.some(item => item.confmAt === 'R')) { | |
124 | + return 'rejected'; | |
125 | + } | |
126 | + | |
127 | + // 전부 승인된 경우 > 승인 | |
128 | + if (sanctnList.every(item => item.confmAt === 'A')) { | |
129 | + return 'approved'; | |
130 | + } | |
131 | + | |
132 | + // 그 외 > 대기 | |
133 | + return 'waiting'; | |
134 | + }, | |
135 | + | |
136 | + // 작성자 여부 | |
137 | + isApplicant() { | |
138 | + return this.vacationInfo.userId === this.$store.state.userInfo.userId; | |
139 | + }, | |
140 | + | |
141 | + // 결재자 여부 | |
142 | + isConsultationApprover() { | |
143 | + const sanctnList = this.vacationInfo.sanctnList; | |
144 | + const mySanctn = sanctnList.find( | |
145 | + item => item.confmerId == this.$store.state.userInfo.userId | |
146 | + ); | |
147 | + return mySanctn && mySanctn.confmAt === 'W' && this.pageMode === 'sanctns'; | |
148 | + }, | |
149 | + }, | |
150 | + | |
151 | + async created() { | |
152 | + this.pageId = this.$route.query.id; | |
153 | + this.pageMode = this.$route.query.type; | |
154 | + if (this.$isEmpty(this.pageId)) { | |
155 | + alert("게시물이 존재하지 않습니다."); | |
156 | + this.handleNavigation('list'); | |
157 | + } | |
158 | + }, | |
159 | + | |
160 | + mounted() { | |
161 | + this.fetchData(); // 휴가 정보 조회 | |
162 | + }, | |
163 | + | |
164 | + methods: { | |
165 | + // 휴가 정보 조회 | |
166 | + async fetchData() { | |
167 | + try { | |
168 | + const response = await findVcatnProc(this.pageId); | |
169 | + const result = response.data.data; | |
170 | + | |
171 | + this.vacationInfo = result; | |
172 | + } catch (error) { | |
173 | + this.handleError(error); | |
174 | + this.handleNavigation('list'); | |
175 | + } | |
176 | + }, | |
177 | + | |
178 | + // 결재 승인/반려 처리 | |
179 | + async handleApproval(value) { | |
180 | + try { | |
181 | + const sanctnList = this.vacationInfo.sanctnList; | |
182 | + const sanctn = sanctnList.find(item => item.confmerId == this.$store.state.userInfo.userId); | |
183 | + | |
184 | + if (!sanctn) { | |
185 | + alert('결재 권한이 없습니다.'); | |
186 | + return; | |
187 | + } | |
188 | + | |
189 | + const data = { | |
190 | + sanctnOrdr: sanctn.sanctnOrdr, | |
191 | + sanctnIem: sanctn.sanctnIem, | |
192 | + sanctnMbyId: this.pageId, | |
193 | + confmAt: value, | |
194 | + returnResn: this.returnResn, | |
195 | + }; | |
196 | + | |
197 | + await updateConfmAtProc(sanctn.sanctnId, data); | |
198 | + alert("승인했습니다."); | |
199 | + this.fetchData(); | |
200 | + } catch (error) { | |
201 | + this.handleError(error); | |
202 | + } | |
203 | + }, | |
204 | + | |
205 | + // 반려 결재 핸들러 | |
206 | + async handleRejection(reason) { | |
207 | + this.returnResn = reason; | |
208 | + await this.handleApproval('R'); | |
209 | + this.showConsultationPopup = false; | |
210 | + }, | |
211 | + | |
212 | + // 휴가 취소 (완전 삭제) | |
213 | + async handleDelete() { | |
214 | + const isCheck = confirm("휴가 신청을 취소하시겠습니까?"); | |
215 | + if (!isCheck) return; | |
216 | + | |
217 | + try { | |
218 | + await deleteVcatnProc(this.pageId); | |
219 | + alert("휴가 신청이 취소되었습니다."); | |
220 | + this.handleNavigation('list'); | |
221 | + } catch (error) { | |
222 | + this.handleError(error); | |
223 | + } | |
224 | + }, | |
225 | + | |
226 | + // 페이지 이동 핸들러 | |
227 | + handleNavigation(type, id) { | |
228 | + const routeMap = { | |
229 | + 'list': { name: this.pageMode === 'sanctns' ? 'PendingApprovalListPage' : 'VcatnListPage' }, | |
230 | + 'view': { name: 'VcatnViewPage', query: { id } }, | |
231 | + 'insert': { name: 'VcatnInsertPage', query: this.$isEmpty(id) ? {} : { id } }, | |
232 | + 'reapply': { name: 'VcatnInsertPage', query: { id, type: 'reapply' } }, | |
233 | + }; | |
234 | + | |
235 | + const route = routeMap[type]; | |
236 | + if (route) { | |
237 | + this.$router.push(route); | |
238 | + } else { | |
239 | + alert("올바르지 않은 경로입니다."); | |
240 | + this.$router.push(routeMap['list']); | |
241 | + } | |
242 | + }, | |
243 | + | |
244 | + // 에러 핸들러 | |
245 | + handleError(error) { | |
246 | + const message = error.response?.data?.message || '에러가 발생했습니다.'; | |
247 | + alert(message); | |
248 | + }, | |
249 | + | |
250 | + // 반려 사유 유틸리티 | |
251 | + getRejectionReason(sanctnList) { | |
252 | + const rejectedItem = sanctnList.find(item => item.confmAt === 'R'); | |
253 | + return rejectedItem?.returnResn || '-'; | |
254 | + }, | |
255 | + } | |
256 | +}; | |
257 | +</script>(파일 끝에 줄바꿈 문자 없음) |
--- client/views/pages/Manager/attendance/attendance.vue
+++ client/views/pages/Manager/attendance/attendance.vue
... | ... | @@ -44,7 +44,7 @@ |
44 | 44 |
</summary> |
45 | 45 |
<ul> |
46 | 46 |
<li> |
47 |
- <router-link :to="{ name: 'hyugaStatue' }" exact-active-class="active-link" v-slot="{ isExactActive }"> |
|
47 |
+ <router-link :to="{ name: 'VcatnList' }" exact-active-class="active-link" v-slot="{ isExactActive }"> |
|
48 | 48 |
<p>휴가 현황</p> |
49 | 49 |
<div class="icon" v-if="isExactActive"> |
50 | 50 |
<img :src="menuicon" alt=""> |
... | ... | @@ -52,7 +52,7 @@ |
52 | 52 |
</router-link> |
53 | 53 |
</li> |
54 | 54 |
<li> |
55 |
- <router-link :to="{ name: 'hyugaInsert' }" exact-active-class="active-link" v-slot="{ isExactActive }"> |
|
55 |
+ <router-link :to="{ name: 'VcatnInsert' }" exact-active-class="active-link" v-slot="{ isExactActive }"> |
|
56 | 56 |
<p>휴가 신청</p> |
57 | 57 |
<div class="icon" v-if="isExactActive"> |
58 | 58 |
<img :src="menuicon" alt=""> |
--- client/views/pages/Manager/attendance/buseoAttendance.vue
+++ client/views/pages/Manager/attendance/buseoAttendance.vue
... | ... | @@ -28,7 +28,7 @@ |
28 | 28 |
</div> |
29 | 29 |
</div> |
30 | 30 |
</div> |
31 |
- |
|
31 |
+ |
|
32 | 32 |
<!-- Table --> |
33 | 33 |
<div class="tbl-wrap"> |
34 | 34 |
<table id="myTable" class="tbl data buseo"> |
... | ... | @@ -67,40 +67,40 @@ |
67 | 67 |
</tr> |
68 | 68 |
</tbody> |
69 | 69 |
</table> |
70 |
- |
|
70 |
+ |
|
71 | 71 |
</div> |
72 | 72 |
<div class="pagination"> |
73 | 73 |
<ul> |
74 | 74 |
<!-- 왼쪽 화살표 (이전 페이지) --> |
75 |
- <li |
|
76 |
- class="arrow" |
|
77 |
- :class="{ disabled: currentPage === 1 }" |
|
75 |
+ <li |
|
76 |
+ class="arrow" |
|
77 |
+ :class="{ disabled: currentPage === 1 }" |
|
78 | 78 |
@click="changePage(currentPage - 1)" |
79 | 79 |
> |
80 | 80 |
< |
81 | 81 |
</li> |
82 | 82 |
|
83 | 83 |
<!-- 페이지 번호 --> |
84 |
- <li |
|
85 |
- v-for="page in totalPages" |
|
86 |
- :key="page" |
|
87 |
- :class="{ active: currentPage === page }" |
|
84 |
+ <li |
|
85 |
+ v-for="page in totalPages" |
|
86 |
+ :key="page" |
|
87 |
+ :class="{ active: currentPage === page }" |
|
88 | 88 |
@click="changePage(page)" |
89 | 89 |
> |
90 | 90 |
{{ page }} |
91 | 91 |
</li> |
92 | 92 |
|
93 | 93 |
<!-- 오른쪽 화살표 (다음 페이지) --> |
94 |
- <li |
|
95 |
- class="arrow" |
|
96 |
- :class="{ disabled: currentPage === totalPages }" |
|
94 |
+ <li |
|
95 |
+ class="arrow" |
|
96 |
+ :class="{ disabled: currentPage === totalPages }" |
|
97 | 97 |
@click="changePage(currentPage + 1)" |
98 | 98 |
> |
99 | 99 |
> |
100 | 100 |
</li> |
101 | 101 |
</ul> |
102 | 102 |
</div> |
103 |
- |
|
103 |
+ |
|
104 | 104 |
</div> |
105 | 105 |
</div> |
106 | 106 |
</div> |
... | ... | @@ -190,7 +190,7 @@ |
190 | 190 |
}, |
191 | 191 |
goToPage(type) { |
192 | 192 |
if (type === '휴가') { |
193 |
- this.$router.push({ name: 'HyugaDetail' }); |
|
193 |
+ this.$router.push({ name: 'VcatnView' }); |
|
194 | 194 |
} else if (type === '출장') { |
195 | 195 |
this.$router.push({ name: 'ChuljangDetail' }); |
196 | 196 |
} |
--- client/views/pages/Manager/attendance/hyugaStatue.vue
... | ... | @@ -1,269 +0,0 @@ |
1 | -<template> | |
2 | - <div class="col-lg-12"> | |
3 | - <div class="card"> | |
4 | - <div class="card-body"> | |
5 | - <h2 class="card-title">휴가 현황</h2> | |
6 | - <div class="form-group"> | |
7 | - <div class="form-conts"> | |
8 | - <div class="form-conts datepicker-conts"> | |
9 | - <div class="datepicker-input"> | |
10 | - <input type="date" class="form-control datepicker cal" v-model="request.bgnde" style="max-width: 200px;" @change="changeDate" /> | |
11 | - <mark>~</mark> | |
12 | - <input type="date" class="form-control datepicker cal" v-model="request.endde" style="max-width: 200px;" @change="changeDate" /> | |
13 | - </div> | |
14 | - </div> | |
15 | - </div> | |
16 | - </div> | |
17 | - <div class="boxs"> | |
18 | - <div class="color-boxs"> | |
19 | - <div v-for="(item, idx) of useSummary" :key="idx" :class="getBoxClass(idx)" @click="fnFindUseList(item)"> | |
20 | - <h3>{{ item.name }}</h3> {{ item.value }} | |
21 | - </div> | |
22 | - <div v-for="(item, idx) of notUseSummary" :key="idx" :class="getBoxClass(useSummary.length + idx)" @click="fnFindNotUseList(item.code)"> | |
23 | - <h3>{{ item.codeNm }}</h3> {{ item.value }} | |
24 | - </div> | |
25 | - </div> | |
26 | - </div> | |
27 | - <div class="tbl-wrap"> | |
28 | - <table id="myTable" class="tbl data"> | |
29 | - <colgroup> | |
30 | - <col style="width: 15%;"> | |
31 | - <col style="width: 40%;"> | |
32 | - <col style="width: 30%;"> | |
33 | - <col style="width: 15%;"> | |
34 | - </colgroup> | |
35 | - <thead> | |
36 | - <tr> | |
37 | - <th>구분</th> | |
38 | - <th>기간</th> | |
39 | - <th>신청일</th> | |
40 | - <th>상태</th> | |
41 | - </tr> | |
42 | - </thead> | |
43 | - <tbody> | |
44 | - <template v-if="lists.length > 0"> | |
45 | - <tr v-for="(item, idx) in lists" :key="idx" @click="fnMoveTo('view', item.vcatnId)"> | |
46 | - <td>{{ item.vcatnKndNm }}</td> | |
47 | - <td>{{ $formattedDates(item) }}</td> | |
48 | - <td>{{ item.rgsde }}</td> | |
49 | - <td>{{ item.confmAtNm }}</td> | |
50 | - </tr> | |
51 | - </template> | |
52 | - <tr v-else> | |
53 | - <td colspan="4">해당 기간 내 등록된 휴가가 없습니다.</td> | |
54 | - </tr> | |
55 | - </tbody> | |
56 | - </table> | |
57 | - </div> | |
58 | - <Pagination :search="request" @onChange="fnChangeCurrentPage" /> | |
59 | - </div> | |
60 | - </div> | |
61 | - </div> | |
62 | -</template> | |
63 | -<script> | |
64 | -import { SearchOutlined } from '@ant-design/icons-vue'; | |
65 | -import Pagination from '../../../component/Pagination.vue'; | |
66 | -// API | |
67 | -import { findVcatnsSummary, findVcatnsProc } from '../../../../resources/api/vcatn'; | |
68 | - | |
69 | -export default { | |
70 | - components: { | |
71 | - SearchOutlined, | |
72 | - Pagination | |
73 | - }, | |
74 | - | |
75 | - data() { | |
76 | - return { | |
77 | - photoicon: "/client/resources/img/photo_icon.png", | |
78 | - | |
79 | - color: ['blue', 'red', 'green', 'purple', 'orange'], | |
80 | - colorMap: [], | |
81 | - | |
82 | - useSummary: [], // 연차 소모성 휴가 | |
83 | - notUseSummary: [], // 연차 비 소모성 휴가 | |
84 | - | |
85 | - request: { | |
86 | - vcatnKnds: null, // 휴가 종류 | |
87 | - bgnde: null, // 시작일 | |
88 | - endde: null, // 종료일 | |
89 | - }, | |
90 | - lists: [], | |
91 | - }; | |
92 | - }, | |
93 | - | |
94 | - computed: {}, | |
95 | - | |
96 | - created() { }, | |
97 | - | |
98 | - mounted() { | |
99 | - this.findSummary(); // 현황 조회 | |
100 | - this.findList(); // 목록 조회 | |
101 | - }, | |
102 | - | |
103 | - methods: { | |
104 | - // 현황 조회 | |
105 | - async findSummary() { | |
106 | - const vm = this; | |
107 | - try { | |
108 | - const response = await findVcatnsSummary(vm.request); | |
109 | - const result = response.data.data; | |
110 | - | |
111 | - this.request.bgnde = new Date(result.data.bgnde).toISOString().split('T')[0]; | |
112 | - this.request.endde = new Date(result.data.endde).toISOString().split('T')[0]; | |
113 | - if (result.data.yryc < 1) { | |
114 | - this.request.endde = new Date().toISOString().split('T')[0]; | |
115 | - } | |
116 | - | |
117 | - let yrycCyfdCo = result.data.yrycCyfdCo; | |
118 | - let totalCo = result.data.yrycCo + yrycCyfdCo; | |
119 | - let useCo = result.useSummary.useCo; | |
120 | - let notUseCo = totalCo - useCo; | |
121 | - | |
122 | - this.useSummary = []; // 초기화 | |
123 | - this.useSummary.push({ key: 'totalCo', name: '전체', value: totalCo + '(' + yrycCyfdCo + ')' }); | |
124 | - this.useSummary.push({ key: 'useCo', name: '사용', value: useCo, codeList: result.useSummary.codeList }); | |
125 | - this.useSummary.push({ key: 'notUseCo', name: '미사용', value: notUseCo }); | |
126 | - | |
127 | - this.notUseSummary = []; // 초기화 | |
128 | - this.notUseSummary = result.notUseSummary; | |
129 | - | |
130 | - this.colorMap = []; // 초기화 | |
131 | - const keys = Object.keys(this.notUseSummary); | |
132 | - for (let i = 0; i < Object.keys(this.notUseSummary).length; i++) { | |
133 | - this.colorMap.push({ | |
134 | - code: keys[i].code, | |
135 | - class: this.color[this.useSummary.length + i], | |
136 | - }) | |
137 | - } | |
138 | - } catch (error) { | |
139 | - if (error.response) { | |
140 | - alert(error.response.data.message); | |
141 | - } else { | |
142 | - alert("에러가 발생했습니다."); | |
143 | - } | |
144 | - console.error(error.message); | |
145 | - }; | |
146 | - }, | |
147 | - | |
148 | - // 목록 조회 | |
149 | - async findList() { | |
150 | - const vm = this; | |
151 | - try { | |
152 | - delete vm.request.vcatnKndList; | |
153 | - | |
154 | - const response = await findVcatnsProc(vm.request); | |
155 | - const result = response.data.data; | |
156 | - | |
157 | - vm.lists = result.lists; | |
158 | - vm.request = result.search; | |
159 | - } catch (error) { | |
160 | - if (error.response) { | |
161 | - alert(error.response.data.message); | |
162 | - } else { | |
163 | - alert("에러가 발생했습니다."); | |
164 | - } | |
165 | - console.error(error.message); | |
166 | - }; | |
167 | - }, | |
168 | - | |
169 | - // 현황에 클래스 부여 | |
170 | - getBoxClass(idx) { | |
171 | - if (idx === 0) return 'box'; | |
172 | - return `box ${this.color[(idx - 1) % this.color.length]}`; | |
173 | - }, | |
174 | - | |
175 | - // 날짜 선택 | |
176 | - changeDate() { | |
177 | - if (!this.request.bgnde || !this.request.endde) { | |
178 | - return; | |
179 | - } | |
180 | - | |
181 | - const startDate = new Date(this.request.bgnde); | |
182 | - const endDate = new Date(this.request.endde); | |
183 | - if (endDate < startDate) { | |
184 | - alert('종료일은 시작일보다 이전일 수 없습니다.'); | |
185 | - this.request.endde = this.request.bgnde; | |
186 | - return; | |
187 | - } | |
188 | - | |
189 | - this.findList(); // 목록 조회 | |
190 | - }, | |
191 | - | |
192 | - // 목록 필터 조회 | |
193 | - fnFindUseList(item) { | |
194 | - if (item.key === 'totalCo') { | |
195 | - this.request.vcatnKnds = null; | |
196 | - } else if (item.key === 'useCo') { | |
197 | - this.request.vcatnKnds = item.codeList.map(code => code.code).join(", "); | |
198 | - } else { | |
199 | - return; | |
200 | - } | |
201 | - | |
202 | - this.findList(); // 목록 조회 | |
203 | - }, | |
204 | - | |
205 | - // 목록 필터 조회 | |
206 | - fnFindNotUseList(vcatnKnd) { | |
207 | - this.request.vcatnKnds = vcatnKnd; | |
208 | - | |
209 | - this.findList(); // 목록 조회 | |
210 | - }, | |
211 | - | |
212 | - // 페이지 이동 | |
213 | - fnChangeCurrentPage(currentPage) { | |
214 | - this.request.currentPage = Number(currentPage); | |
215 | - this.$nextTick(() => { | |
216 | - this.findList(); | |
217 | - }); | |
218 | - }, | |
219 | - | |
220 | - // 페이지 이동 | |
221 | - fnMoveTo(type, id) { | |
222 | - const routes = { | |
223 | - 'list': { name: 'hyugaStatue' }, | |
224 | - 'view': { name: 'HyugaDetail', query: { id } }, | |
225 | - 'edit': { name: 'hyugaInsert', query: this.$isEmpty(id) ? {} : { id } }, | |
226 | - }; | |
227 | - | |
228 | - if (routes[type]) { | |
229 | - if (!this.$isEmpty(this.pageId) && type === 'list') { | |
230 | - this.$router.push({ name: 'HyugaDetail', query: { id: this.pageId } }); | |
231 | - } | |
232 | - this.$router.push(routes[type]); | |
233 | - } else { | |
234 | - alert("올바르지 않은 경로를 요청하여 목록으로 이동합니다."); | |
235 | - this.$router.push(routes['list']); | |
236 | - } | |
237 | - }, | |
238 | - }, | |
239 | -}; | |
240 | -</script> | |
241 | -<style scoped> | |
242 | -tr { | |
243 | - cursor: pointer; | |
244 | -} | |
245 | - | |
246 | -.box { | |
247 | - cursor: pointer; | |
248 | -} | |
249 | - | |
250 | -.vcatnKnd.red { | |
251 | - color: #E92727 !important; | |
252 | -} | |
253 | - | |
254 | -.vcatnKnd.green { | |
255 | - color: #3C97AB !important; | |
256 | -} | |
257 | - | |
258 | -.vcatnKnd.blue { | |
259 | - color: #1D75E1 !important; | |
260 | -} | |
261 | - | |
262 | -.vcatnKnd.purple { | |
263 | - color: #A36CD4 !important; | |
264 | -} | |
265 | - | |
266 | -.vcatnKnd.orange { | |
267 | - color: #F7941C !important; | |
268 | -} | |
269 | -</style>(파일 끝에 줄바꿈 문자 없음) |
--- client/views/pages/Manager/financial/ChuljangCostList.vue
+++ client/views/pages/Manager/financial/ChuljangCostList.vue
... | ... | @@ -129,9 +129,9 @@ |
129 | 129 |
methods: { |
130 | 130 |
goToDetailPage(item) { |
131 | 131 |
// item.id 또는 다른 식별자를 사용하여 URL을 구성할 수 있습니다. |
132 |
- this.$router.push({ |
|
132 |
+ this.$router.push({ |
|
133 | 133 |
name: 'employeeSalaryDetail', |
134 |
- query: { id: item.id } |
|
134 |
+ query: { id: item.id } |
|
135 | 135 |
}); |
136 | 136 |
}, |
137 | 137 |
formatCurrency(amount) { |
... | ... | @@ -160,7 +160,7 @@ |
160 | 160 |
}, |
161 | 161 |
goToPage(type) { |
162 | 162 |
if (type === '휴가') { |
163 |
- this.$router.push({ name: 'HyugaInsert' }); |
|
163 |
+ this.$router.push({ name: 'VcatnInsert' }); |
|
164 | 164 |
} else if (type === '출장') { |
165 | 165 |
this.$router.push({ name: 'ChuljangDetail' }); |
166 | 166 |
} |
--- client/views/pages/Manager/financial/MeetingCostList.vue
+++ client/views/pages/Manager/financial/MeetingCostList.vue
... | ... | @@ -130,7 +130,7 @@ |
130 | 130 |
}, |
131 | 131 |
goToPage(type) { |
132 | 132 |
if (type === '휴가') { |
133 |
- this.$router.push({ name: 'HyugaInsert' }); |
|
133 |
+ this.$router.push({ name: 'VcatnInsert' }); |
|
134 | 134 |
} else if (type === '출장') { |
135 | 135 |
this.$router.push({ name: 'ChuljangDetail' }); |
136 | 136 |
} |
--- client/views/pages/Manager/financial/employeeSalaryList.vue
+++ client/views/pages/Manager/financial/employeeSalaryList.vue
... | ... | @@ -154,7 +154,7 @@ |
154 | 154 |
}, |
155 | 155 |
goToPage(type) { |
156 | 156 |
if (type === '휴가') { |
157 |
- this.$router.push({ name: 'HyugaInsert' }); |
|
157 |
+ this.$router.push({ name: 'VcatnInsert' }); |
|
158 | 158 |
} else if (type === '출장') { |
159 | 159 |
this.$router.push({ name: 'ChuljangDetail' }); |
160 | 160 |
} |
--- client/views/pages/Manager/financial/salaryList.vue
+++ client/views/pages/Manager/financial/salaryList.vue
... | ... | @@ -143,7 +143,7 @@ |
143 | 143 |
}, |
144 | 144 |
goToPage(type) { |
145 | 145 |
if (type === '명세서') { |
146 |
- this.$router.push({ name: 'HyugaInsert' }); |
|
146 |
+ this.$router.push({ name: 'VcatnInsert' }); |
|
147 | 147 |
} else if (type === '출장') { |
148 | 148 |
this.$router.push({ name: 'ChuljangDetail' }); |
149 | 149 |
} |
--- client/views/pages/Manager/sanctn/MyApprovalRequestList.vue
+++ client/views/pages/Manager/sanctn/MyApprovalRequestList.vue
... | ... | @@ -156,7 +156,7 @@ |
156 | 156 |
const approvalType = id.split('_')[0]; |
157 | 157 |
|
158 | 158 |
if (approvalType === "VCATN") { |
159 |
- this.$router.push({ name: 'HyugaDetail', query: { id } }); |
|
159 |
+ this.$router.push({ name: 'VcatnView', query: { id } }); |
|
160 | 160 |
} if (approvalType === "BSRP") { |
161 | 161 |
this.$router.push({ name: 'ChuljangDetailAll', query: { id } }); |
162 | 162 |
} |
... | ... | @@ -165,7 +165,7 @@ |
165 | 165 |
// 등록 페이지 이동 |
166 | 166 |
handleRegistrationNavigation(type) { |
167 | 167 |
if (type === "휴가") { |
168 |
- this.$router.push({ name: 'hyugaInsert' }); |
|
168 |
+ this.$router.push({ name: 'VcatnInsert' }); |
|
169 | 169 |
} else if (type === "출장") { |
170 | 170 |
this.$router.push({ name: 'ChuljangInsert' }); |
171 | 171 |
} |
--- client/views/pages/Manager/sanctn/PendingApprovalList.vue
+++ client/views/pages/Manager/sanctn/PendingApprovalList.vue
... | ... | @@ -40,12 +40,17 @@ |
40 | 40 |
</tr> |
41 | 41 |
</thead> |
42 | 42 |
<tbody> |
43 |
- <tr v-for="(item, idx) in pendingApprovalList" :key="idx" @click="handleDetailNavigation(item.sanctnMbyId)"> |
|
44 |
- <td>{{ item.sanctnIemNm }}</td> |
|
45 |
- <td>{{ item.sanctnSeNm }}</td> |
|
46 |
- <td>{{ item.registerNm }}</td> |
|
47 |
- <td>{{ $formattedDates(item) }}</td> |
|
48 |
- <td>{{ item.rgsde }}</td> |
|
43 |
+ <template v-if="pendingApprovalList.length > 0"> |
|
44 |
+ <tr v-for="(item, idx) in pendingApprovalList" :key="idx" @click="handleDetailNavigation(item.sanctnMbyId)"> |
|
45 |
+ <td>{{ item.sanctnIemNm }}</td> |
|
46 |
+ <td>{{ item.sanctnSeNm }}</td> |
|
47 |
+ <td>{{ item.registerNm }}</td> |
|
48 |
+ <td>{{ $formattedDates(item) }}</td> |
|
49 |
+ <td>{{ item.rgsde }}</td> |
|
50 |
+ </tr> |
|
51 |
+ </template> |
|
52 |
+ <tr v-else> |
|
53 |
+ <td colspan="6">게시물이 존재하지 않습니다.</td> |
|
49 | 54 |
</tr> |
50 | 55 |
</tbody> |
51 | 56 |
</table> |
... | ... | @@ -96,13 +101,18 @@ |
96 | 101 |
</tr> |
97 | 102 |
</thead> |
98 | 103 |
<tbody> |
99 |
- <tr v-for="(item, index) in approvalHistoryList" :key="index" class="expired" @click="handleDetailNavigation(item.sanctnMbyId)"> |
|
100 |
- <td>{{ item.sanctnIemNm }}</td> |
|
101 |
- <td>{{ item.sanctnSeNm }}</td> |
|
102 |
- <td>{{ item.registerNm }}</td> |
|
103 |
- <td>{{ $formattedDates(item) }}</td> |
|
104 |
- <td>{{ item.rgsde }}</td> |
|
105 |
- <td>{{ item.confmAtNm }}</td> |
|
104 |
+ <template v-if="approvalHistoryList.length > 0"> |
|
105 |
+ <tr v-for="(item, index) in approvalHistoryList" :key="index" class="expired" @click="handleDetailNavigation(item.sanctnMbyId)"> |
|
106 |
+ <td>{{ item.sanctnIemNm }}</td> |
|
107 |
+ <td>{{ item.sanctnSeNm }}</td> |
|
108 |
+ <td>{{ item.registerNm }}</td> |
|
109 |
+ <td>{{ $formattedDates(item) }}</td> |
|
110 |
+ <td>{{ item.rgsde }}</td> |
|
111 |
+ <td>{{ item.confmAtNm }}</td> |
|
112 |
+ </tr> |
|
113 |
+ </template> |
|
114 |
+ <tr v-else> |
|
115 |
+ <td colspan="6">게시물이 존재하지 않습니다.</td> |
|
106 | 116 |
</tr> |
107 | 117 |
</tbody> |
108 | 118 |
</table> |
... | ... | @@ -129,8 +139,8 @@ |
129 | 139 |
return { |
130 | 140 |
sectionIcon: "/client/resources/img/h3icon.png", |
131 | 141 |
|
132 |
- yearOptions: [2023, 2024, 2025], // 연도 목록 |
|
133 |
- monthOptions: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], // 월 목록 |
|
142 |
+ yearOptions: [], |
|
143 |
+ monthOptions: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], |
|
134 | 144 |
|
135 | 145 |
// 승인 대기 목록 |
136 | 146 |
pendingApprovalList: [], |
... | ... | @@ -140,6 +150,7 @@ |
140 | 150 |
isConfmAt: true, // 변경불가 (고정값) |
141 | 151 |
searchText: '', |
142 | 152 |
currentUserId: this.$store.state.userInfo.userId, // 변경불가 (고정값) |
153 |
+ recordSize: 5, |
|
143 | 154 |
}, |
144 | 155 |
// 승인 이력 목록 |
145 | 156 |
approvalHistoryList: [], |
... | ... | @@ -149,6 +160,7 @@ |
149 | 160 |
isConfmAt: false, // 변경불가 (고정값) |
150 | 161 |
searchText: '', |
151 | 162 |
currentUserId: this.$store.state.userInfo.userId, // 변경불가 (고정값) |
163 |
+ recordSize: 5, |
|
152 | 164 |
}, |
153 | 165 |
}; |
154 | 166 |
}, |
... | ... | @@ -227,7 +239,7 @@ |
227 | 239 |
const approvalType = id.split('_')[0]; |
228 | 240 |
|
229 | 241 |
if (approvalType === "VCATN") { |
230 |
- this.$router.push({ name: 'HyugaDetail', query: { id, type: 'sanctns' } }); |
|
242 |
+ this.$router.push({ name: 'VcatnViewPage', query: { id, type: 'sanctns' } }); |
|
231 | 243 |
} if (approvalType === "BSRP") { |
232 | 244 |
this.$router.push({ name: 'BsrpViewPage', query: { id, type: 'sanctns' } }); |
233 | 245 |
} |
--- client/views/pages/Manager/task/projectTuib.vue
+++ client/views/pages/Manager/task/projectTuib.vue
... | ... | @@ -26,7 +26,7 @@ |
26 | 26 |
</div> |
27 | 27 |
</div> |
28 | 28 |
</div> |
29 |
- |
|
29 |
+ |
|
30 | 30 |
<!-- Table --> |
31 | 31 |
<div class="tbl-wrap"> |
32 | 32 |
<table id="myTable" class="tbl data buseo"> |
... | ... | @@ -58,40 +58,40 @@ |
58 | 58 |
</tr> |
59 | 59 |
</tbody> |
60 | 60 |
</table> |
61 |
- |
|
61 |
+ |
|
62 | 62 |
</div> |
63 | 63 |
<div class="pagination"> |
64 | 64 |
<ul> |
65 | 65 |
<!-- 왼쪽 화살표 (이전 페이지) --> |
66 |
- <li |
|
67 |
- class="arrow" |
|
68 |
- :class="{ disabled: currentPage === 1 }" |
|
66 |
+ <li |
|
67 |
+ class="arrow" |
|
68 |
+ :class="{ disabled: currentPage === 1 }" |
|
69 | 69 |
@click="changePage(currentPage - 1)" |
70 | 70 |
> |
71 | 71 |
< |
72 | 72 |
</li> |
73 | 73 |
|
74 | 74 |
<!-- 페이지 번호 --> |
75 |
- <li |
|
76 |
- v-for="page in totalPages" |
|
77 |
- :key="page" |
|
78 |
- :class="{ active: currentPage === page }" |
|
75 |
+ <li |
|
76 |
+ v-for="page in totalPages" |
|
77 |
+ :key="page" |
|
78 |
+ :class="{ active: currentPage === page }" |
|
79 | 79 |
@click="changePage(page)" |
80 | 80 |
> |
81 | 81 |
{{ page }} |
82 | 82 |
</li> |
83 | 83 |
|
84 | 84 |
<!-- 오른쪽 화살표 (다음 페이지) --> |
85 |
- <li |
|
86 |
- class="arrow" |
|
87 |
- :class="{ disabled: currentPage === totalPages }" |
|
85 |
+ <li |
|
86 |
+ class="arrow" |
|
87 |
+ :class="{ disabled: currentPage === totalPages }" |
|
88 | 88 |
@click="changePage(currentPage + 1)" |
89 | 89 |
> |
90 | 90 |
> |
91 | 91 |
</li> |
92 | 92 |
</ul> |
93 | 93 |
</div> |
94 |
- |
|
94 |
+ |
|
95 | 95 |
</div> |
96 | 96 |
</div> |
97 | 97 |
</div> |
... | ... | @@ -164,7 +164,7 @@ |
164 | 164 |
}, |
165 | 165 |
goToPage(type) { |
166 | 166 |
if (type === '휴가') { |
167 |
- this.$router.push({ name: 'HyugaDetail' }); |
|
167 |
+ this.$router.push({ name: 'VcatnView' }); |
|
168 | 168 |
} else if (type === '출장') { |
169 | 169 |
this.$router.push({ name: 'ChuljangDetail' }); |
170 | 170 |
} |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?