
--- client/resources/api/bsrp.js
+++ client/resources/api/bsrp.js
... | ... | @@ -1,26 +1,31 @@ |
1 |
-import {apiClient} from "./index"; |
|
1 |
+import { apiClient } from "./index"; |
|
2 | 2 |
|
3 |
-// 등록 |
|
3 |
+// 출장 목록 조회 |
|
4 |
+export const findBsrpsProc = data => { |
|
5 |
+ return apiClient.get('/bsrp/findBsrps.json', { params: data }); |
|
6 |
+} |
|
7 |
+ |
|
8 |
+// 출장 상세 조회 |
|
9 |
+export const findBsrpProc = data => { |
|
10 |
+ return apiClient.get(`/bsrp/${data}/findBsrp.json`); |
|
11 |
+} |
|
12 |
+ |
|
13 |
+// 출장 품의 이전 승인자 조회 |
|
14 |
+export const findLastBsrpCnsulProc = () => { |
|
15 |
+ return apiClient.get('/bsrpCnsul/findLastBsrpCnsul.json'); |
|
16 |
+} |
|
17 |
+ |
|
18 |
+// 출장 정보, 출장 품의 등록 |
|
4 | 19 |
export const saveBsrpProc = data => { |
5 | 20 |
return apiClient.post('/bsrp/saveBsrp.json', data); |
6 | 21 |
} |
7 | 22 |
|
8 |
-// 목록 조회 |
|
9 |
-export const bsrpsProc = data => { |
|
10 |
- return apiClient.get('/bsrp/bsrps.json', { params: data }); |
|
23 |
+// 출장 정보, 출장 품의 수정 |
|
24 |
+export const updateBsrpProc = (id, data) => { |
|
25 |
+ return apiClient.put(`/bsrp/${id}/updateBsrp.json`, data); |
|
11 | 26 |
} |
12 | 27 |
|
13 |
-// 상세 조회 |
|
14 |
-export const bsrpProc = data => { |
|
15 |
- return apiClient.get(`/bsrp/${data}/bsrp.json`); |
|
16 |
-} |
|
17 |
- |
|
18 |
-// 수정 |
|
19 |
-export const updateBsrpProc = data => { |
|
20 |
- return apiClient.put('/bsrp/updateBsrp.json', data); |
|
21 |
-} |
|
22 |
- |
|
23 |
-// 삭제 |
|
28 |
+// 출장 정보, 출장 품의 삭제 |
|
24 | 29 |
export const deleteBsrpProc = data => { |
25 | 30 |
return apiClient.delete(`/bsrp/${data}/deleteBsrp.json`); |
26 | 31 |
}(파일 끝에 줄바꿈 문자 없음) |
+++ client/resources/api/bsrpRport.js
... | ... | @@ -0,0 +1,26 @@ |
1 | +import { apiClient, fileClient } from "./index"; | |
2 | + | |
3 | +// 출장 복명 상세 조회 | |
4 | +export const findBsrpRportProc = data => { | |
5 | + return apiClient.get(`/bsrpRport/${data}/findBsrpRport.json`); | |
6 | +} | |
7 | + | |
8 | +// 출장 복명 이전 승인자 조회 | |
9 | +export const findLastBsrpRportProc = () => { | |
10 | + return apiClient.get('/bsrpRport/findLastBsrpRport.json'); | |
11 | +} | |
12 | + | |
13 | +// 출장 복명 등록 | |
14 | +export const saveBsrpRportProc = data => { | |
15 | + return fileClient.post('/bsrpRport/saveBsrpRport.file', data); | |
16 | +} | |
17 | + | |
18 | +// 출장 복명 수정 | |
19 | +export const updateBsrpRport = data => { | |
20 | + return fileClient.put('/bsrpRport/updateBsrpRport.file', data); | |
21 | +} | |
22 | + | |
23 | +// 출장 복명 삭제 | |
24 | +export const deleteBsrpRportProc = data => { | |
25 | + return apiClient.delete(`/bsrpRport/${data}/deleteBsrpRport.json`); | |
26 | +}(파일 끝에 줄바꿈 문자 없음) |
+++ client/resources/api/file.js
... | ... | @@ -0,0 +1,6 @@ |
1 | +import { apiClient } from "./index"; | |
2 | + | |
3 | +// 파일 다운로드 | |
4 | +export const fileDownloadProc = (file) => { | |
5 | + return apiClient.get(`/file/${file.fileId}/${file.ordr}/fileDownload.json`, { responseType: 'blob' }); | |
6 | +}(파일 끝에 줄바꿈 문자 없음) |
--- client/resources/api/sanctns.js
+++ client/resources/api/sanctns.js
... | ... | @@ -8,4 +8,9 @@ |
8 | 8 |
// 승인 대기 목록 조회 |
9 | 9 |
export const findPendingApprovalsProc = data => { |
10 | 10 |
return apiClient.get('/sanctn/findPendingApprovals.json', { params: data }); |
11 |
+} |
|
12 |
+ |
|
13 |
+// 결재 |
|
14 |
+export const updateConfmAtProc = (sanctnId, data) => { |
|
15 |
+ return apiClient.put(`/sanctn/${sanctnId}/updateConfmAt.json`, data); |
|
11 | 16 |
}(파일 끝에 줄바꿈 문자 없음) |
--- client/views/component/Popup/ReturnPopup.vue
+++ client/views/component/Popup/ReturnPopup.vue
... | ... | @@ -1,48 +1,64 @@ |
1 | 1 |
<template> |
2 |
- <div class="popup-overlay" @click.self="$emit('close')"> |
|
3 |
- <div class="popup-content"> |
|
4 |
- <div class="card"> |
|
5 |
- <div class="card-body"> |
|
6 |
- <h2 class="card-title">반려 사유</h2> |
|
7 |
- <textarea name="" id="" class="form-control "></textarea> |
|
8 |
- <div class="buttons"> |
|
9 |
- <button class="btn sm primary" type="submit">등록</button> |
|
10 |
- <button class="btn sm tertiary" type="submit" @click="$emit('close')">취소</button> |
|
11 |
- </div> |
|
12 |
- </div> |
|
13 |
- </div> |
|
14 |
- <button @click="$emit('close')" class="close-btn"> |
|
15 |
- <CloseCircleFilled /> |
|
16 |
- </button> |
|
17 |
- </div> |
|
18 |
- </div> |
|
2 |
+ <div class="popup-overlay" @click.self="handleClose"> |
|
3 |
+ <div class="popup-content"> |
|
4 |
+ <div class="card"> |
|
5 |
+ <div class="card-body"> |
|
6 |
+ <h2 class="card-title">반려 사유</h2> |
|
7 |
+ <textarea v-model="rejectionReason" class="form-control" placeholder="반려 사유를 입력해주세요." /> |
|
8 |
+ <div class="buttons"> |
|
9 |
+ <button type="button" class="btn sm primary" @click="handleConfirm" :disabled="!isValidReason">등록</button> |
|
10 |
+ <button type="button" class="btn sm tertiary" @click="handleClose"> 취소 </button> |
|
11 |
+ </div> |
|
12 |
+ </div> |
|
13 |
+ </div> |
|
14 |
+ <button @click="handleClose" class="close-btn"> |
|
15 |
+ <CloseCircleFilled /> |
|
16 |
+ </button> |
|
17 |
+ </div> |
|
18 |
+ </div> |
|
19 | 19 |
</template> |
20 | 20 |
<script> |
21 |
-import { SearchOutlined, CloseCircleFilled } from '@ant-design/icons-vue'; |
|
21 |
+import { CloseCircleFilled } from '@ant-design/icons-vue'; |
|
22 | 22 |
|
23 | 23 |
export default { |
24 |
- data() { |
|
25 |
- return { |
|
26 |
- } |
|
27 |
- }, |
|
28 |
- components: { |
|
29 |
- SearchOutlined, CloseCircleFilled |
|
24 |
+ components: { |
|
25 |
+ CloseCircleFilled |
|
30 | 26 |
}, |
27 |
+ |
|
28 |
+ emits: ['close', 'confirm'], |
|
29 |
+ |
|
30 |
+ data() { |
|
31 |
+ return { |
|
32 |
+ rejectionReason: '' |
|
33 |
+ } |
|
34 |
+ }, |
|
35 |
+ |
|
36 |
+ computed: { |
|
37 |
+ isValidReason() { |
|
38 |
+ return this.rejectionReason.trim().length > 0; |
|
39 |
+ } |
|
40 |
+ }, |
|
41 |
+ |
|
31 | 42 |
methods: { |
32 |
- selectCar(item) { |
|
33 |
- this.$emit('select', item); // 부모에게 데이터 전달 |
|
34 |
- }, |
|
35 |
- |
|
43 |
+ handleClose() { |
|
44 |
+ this.$emit('close'); |
|
45 |
+ }, |
|
46 |
+ |
|
47 |
+ handleConfirm() { |
|
48 |
+ if (this.isValidReason) { |
|
49 |
+ this.$emit('confirm', this.rejectionReason.trim()); |
|
50 |
+ } |
|
51 |
+ }, |
|
36 | 52 |
} |
37 | 53 |
} |
38 | 54 |
</script> |
39 | 55 |
<style scoped> |
40 | 56 |
.popup-content { |
41 |
- width: 50%; |
|
42 |
- |
|
57 |
+ width: 50%; |
|
43 | 58 |
} |
44 |
-.form-control{ |
|
45 |
- border-color: #C7CFE3; |
|
46 |
- min-height: 20rem; |
|
47 |
- } |
|
59 |
+ |
|
60 |
+.form-control { |
|
61 |
+ border-color: #C7CFE3; |
|
62 |
+ min-height: 20rem; |
|
63 |
+} |
|
48 | 64 |
</style>(파일 끝에 줄바꿈 문자 없음) |
--- client/views/component/Sanctn/SanctnViewList.vue
+++ client/views/component/Sanctn/SanctnViewList.vue
... | ... | @@ -24,8 +24,6 @@ |
24 | 24 |
</template> |
25 | 25 |
<script> |
26 | 26 |
export default { |
27 |
- name: 'SanctnViewList', |
|
28 |
- |
|
29 | 27 |
props: { |
30 | 28 |
sanctns: { |
31 | 29 |
type: Array, |
+++ client/views/component/bsrp/BsrpRportView.vue
... | ... | @@ -0,0 +1,305 @@ |
1 | +<template> | |
2 | + <div class="form-card"> | |
3 | + <h1>출장복명서</h1> | |
4 | + <SanctnViewList v-if="bsrpRport.sanctnList.length > 0" :sanctns="bsrpRport.sanctnList" /> | |
5 | + <div class="tbl-wrap row g-3 needs-validation detail"> | |
6 | + <table class="tbl"> | |
7 | + <tbody> | |
8 | + <tr> | |
9 | + <th>복명내용</th> | |
10 | + <td> | |
11 | + <ViewerComponent :content="bsrpRport.cn" /> | |
12 | + </td> | |
13 | + </tr> | |
14 | + <tr> | |
15 | + <th>여비계산</th> | |
16 | + <td> | |
17 | + <template v-if="bsrpRport.bsrpTrvctList.length > 0"> | |
18 | + <div v-for="(item, idx) of bsrpRport.bsrpTrvctList" :key="idx"> | |
19 | + <p> | |
20 | + <span>{{ item.seNm + '결재' }} ({{ getPaymentEntityText(item.se, item.setleMbyId) }})</span> | |
21 | + <span>{{ item.tyNm }}</span> | |
22 | + <span>{{ formatAmount(item.amount) }}</span> | |
23 | + <button type="button" v-if="item.ordr" @click="handleDownload(item.ordr)">{{ getFileNm(item.ordr) }}</button> | |
24 | + <span v-else>영수증 없음</span> | |
25 | + </p> | |
26 | + </div> | |
27 | + </template> | |
28 | + </td> | |
29 | + </tr> | |
30 | + <tr> | |
31 | + <th>복명 신청일</th> | |
32 | + <td>{{ bsrpRport.rgsde }}</td> | |
33 | + </tr> | |
34 | + <tr v-if="reportSanctnStatus === 'rejected'"> | |
35 | + <th>복명 반려사유</th> | |
36 | + <td>{{ getRejectionReason(bsrpRport.sanctnList) }}</td> | |
37 | + </tr> | |
38 | + </tbody> | |
39 | + </table> | |
40 | + </div> | |
41 | + </div> | |
42 | + <div class="buttons"> | |
43 | + <template v-if="isReportApprover"> | |
44 | + <button type="button" class="btn sm primary" @click="handleApproval('A')">승인</button> | |
45 | + <button type="button" class="btn sm btn-red" @click="showReportPopup = true">반려</button> | |
46 | + </template> | |
47 | + <template v-else-if="isApplicant"> | |
48 | + <template v-if="reportSanctnStatus === 'waiting'"> | |
49 | + <button type="button" class="btn sm btn-red" @click="handleDeleteReport">복명서 취소</button> | |
50 | + <button type="button" class="btn sm secondary" @click="handleNavigation('bsrpRportInsert', pageId)">복명서 수정</button> | |
51 | + </template> | |
52 | + <template v-if="reportSanctnStatus === 'rejected'"> | |
53 | + <button type="button" class="btn sm secondary" @click="handleNavigation('bsrpRportReapply', pageId)">복명서 재신청</button> | |
54 | + </template> | |
55 | + </template> | |
56 | + <button type="button" class="btn sm tertiary" @click="handleNavigation('list')">목록</button> | |
57 | + </div> | |
58 | + <ReturnPopup v-if="showReportPopup" @close="showReportPopup = false" @confirm="handleRejection" /> | |
59 | +</template> | |
60 | +<script> | |
61 | +import ReturnPopup from '../Popup/ReturnPopup.vue'; | |
62 | +import SanctnViewList from '../Sanctn/SanctnViewList.vue'; | |
63 | +import ViewerComponent from '../editor/ViewerComponent.vue'; | |
64 | +// API | |
65 | +import { findBsrpRportProc, deleteBsrpRportProc } from '../../../resources/api/bsrpRport'; | |
66 | +import { updateConfmAtProc } from '../../../resources/api/sanctns'; | |
67 | +import { fileDownloadProc } from '../../../resources/api/file'; | |
68 | + | |
69 | +export default { | |
70 | + name: 'BsrpRportView', | |
71 | + | |
72 | + components: { | |
73 | + ReturnPopup, | |
74 | + SanctnViewList, | |
75 | + ViewerComponent, | |
76 | + }, | |
77 | + | |
78 | + props: { | |
79 | + pageId: { | |
80 | + type: String, | |
81 | + default: null, | |
82 | + }, | |
83 | + pageMode: { | |
84 | + type: String, | |
85 | + default: null, | |
86 | + }, | |
87 | + cards: { | |
88 | + type: Array, | |
89 | + default: () => [], | |
90 | + }, | |
91 | + users: { | |
92 | + type: Array, | |
93 | + default: () => [], | |
94 | + }, | |
95 | + }, | |
96 | + | |
97 | + data() { | |
98 | + return { | |
99 | + showReportPopup: false, | |
100 | + bsrpInfo: { | |
101 | + applcntId: null, | |
102 | + }, | |
103 | + bsrpRport: { | |
104 | + cn: null, | |
105 | + rgsde: null, | |
106 | + sanctnList: [], | |
107 | + bsrpTrvctList: [], | |
108 | + fileList: [], | |
109 | + }, | |
110 | + returnResn: null, | |
111 | + }; | |
112 | + }, | |
113 | + | |
114 | + computed: { | |
115 | + // 결재 상태 | |
116 | + reportSanctnStatus() { | |
117 | + const sanctnList = this.bsrpRport.sanctnList; | |
118 | + if (sanctnList.length === 0) return 'none'; | |
119 | + | |
120 | + // 하나라도 반려된 경우 > 반려 | |
121 | + if (sanctnList.some(item => item.confmAt === 'R')) { | |
122 | + return 'rejected'; | |
123 | + } | |
124 | + | |
125 | + // 전부 승인된 경우 > 승인 | |
126 | + if (sanctnList.every(item => item.confmAt === 'A')) { | |
127 | + return 'approved'; | |
128 | + } | |
129 | + | |
130 | + // 그 외 > 대기 | |
131 | + return 'waiting'; | |
132 | + }, | |
133 | + | |
134 | + // 작성자 여부 | |
135 | + isApplicant() { | |
136 | + return this.bsrpRport.register === this.$store.state.userInfo.userId; | |
137 | + }, | |
138 | + | |
139 | + // 결재자 여부 | |
140 | + isReportApprover() { | |
141 | + const sanctnList = this.bsrpRport.sanctnList; | |
142 | + const mySanctn = sanctnList.find( | |
143 | + item => item.confmerId == this.$store.state.userInfo.userId | |
144 | + ); | |
145 | + return mySanctn && mySanctn.confmAt === 'W' && this.pageMode === 'sanctns'; | |
146 | + }, | |
147 | + }, | |
148 | + | |
149 | + mounted() { | |
150 | + this.fetchData(); // 복명 정보 조회 | |
151 | + }, | |
152 | + | |
153 | + methods: { | |
154 | + // 복명 정보 조회 | |
155 | + async fetchData() { | |
156 | + try { | |
157 | + const response = await findBsrpRportProc(this.pageId); | |
158 | + const result = response.data.data; | |
159 | + | |
160 | + this.bsrpRport = result; | |
161 | + } catch (error) { | |
162 | + const message = error.response?.data?.message || '데이터 조회에 실패했습니다.'; | |
163 | + alert(message); | |
164 | + } | |
165 | + }, | |
166 | + | |
167 | + // 결재 | |
168 | + async handleApproval(value) { | |
169 | + try { | |
170 | + const sanctnList = this.bsrpRport.sanctnList; | |
171 | + const sanctn = sanctnList.find(item => item.confmerId == this.$store.state.userInfo.userId); | |
172 | + | |
173 | + if (!sanctn) { | |
174 | + alert('결재 권한이 없습니다.'); | |
175 | + return; | |
176 | + } | |
177 | + | |
178 | + const data = { | |
179 | + sanctnOrdr: sanctn.sanctnOrdr, | |
180 | + sanctnIem: sanctn.sanctnIem, | |
181 | + sanctnMbyId: this.pageId, | |
182 | + confmAt: value, | |
183 | + returnResn: this.returnResn, | |
184 | + }; | |
185 | + | |
186 | + await updateConfmAtProc(sanctn.sanctnId, data); | |
187 | + const message = value === 'A' ? '승인했습니다.' : '반려했습니다.'; | |
188 | + alert(message); | |
189 | + this.fetchData(); | |
190 | + } catch (error) { | |
191 | + const message = error.response?.data?.message || '승인 처리에 실패했습니다.'; | |
192 | + alert(message); | |
193 | + } | |
194 | + }, | |
195 | + | |
196 | + // 복명 취소 (완전 삭제) | |
197 | + async handleDeleteReport() { | |
198 | + const isCheck = confirm("복명서를 취소하시겠습니까?"); | |
199 | + if (!isCheck) return; | |
200 | + | |
201 | + try { | |
202 | + await deleteBsrpRportProc(this.pageId); | |
203 | + alert("복명서가 취소되었습니다."); | |
204 | + this.fetchData(); | |
205 | + } catch (error) { | |
206 | + const message = error.response?.data?.message || '복명서 취소에 실패했습니다.'; | |
207 | + alert(message); | |
208 | + } | |
209 | + }, | |
210 | + | |
211 | + // 첨부 파일 다운로드 핸들러 | |
212 | + async handleDownload(fileOrdr) { | |
213 | + const selectedFile = this.bsrpRport.fileList.find(file => file.ordr == fileOrdr); | |
214 | + if (!selectedFile) return; | |
215 | + | |
216 | + let url = null; | |
217 | + let link = null; | |
218 | + | |
219 | + try { | |
220 | + const response = await fileDownloadProc(selectedFile); | |
221 | + | |
222 | + let filename = 'downloadFile.bin'; | |
223 | + const filenameRegex = /file[Nn]ame[^;=\n]*=((['"]).*?\2|[^;\n]*)/; | |
224 | + const matches = filenameRegex.exec(response.headers['content-disposition']); | |
225 | + if (matches != null && matches[1]) { | |
226 | + filename = matches[1].replace(/['"]/g, ''); | |
227 | + } | |
228 | + | |
229 | + url = window.URL.createObjectURL(new Blob([response.data])); | |
230 | + link = document.createElement('a'); | |
231 | + link.href = url; | |
232 | + link.setAttribute('download', filename); | |
233 | + document.body.appendChild(link); | |
234 | + link.click(); | |
235 | + } catch (error) { | |
236 | + alert('파일 다운로드 중 오류가 발생했습니다.'); | |
237 | + } finally { | |
238 | + setTimeout(() => { | |
239 | + if (url) { | |
240 | + window.URL.revokeObjectURL(url); | |
241 | + } | |
242 | + if (link && link.parentNode) { | |
243 | + document.body.removeChild(link); | |
244 | + } | |
245 | + }, 100); | |
246 | + } | |
247 | + }, | |
248 | + | |
249 | + // 반려 결재 핸들러 | |
250 | + async handleRejection(reason) { | |
251 | + this.returnResn = reason; | |
252 | + await this.handleApproval('R'); | |
253 | + this.showReportPopup = false; | |
254 | + }, | |
255 | + | |
256 | + // 페이지 이동 핸들러 | |
257 | + handleNavigation(type, id) { | |
258 | + const routeMap = { | |
259 | + 'list': { name: this.pageMode === 'sanctns' ? 'PendingApprovalListPage' : 'BsrpListPage' }, | |
260 | + 'view': { name: 'BsrpViewPage', query: { id } }, | |
261 | + 'bsrpInsert': { name: 'BsrpInsertPage', query: this.$isEmpty(id) ? {} : { id } }, | |
262 | + 'bsrpReapply': { name: 'BsrpInsertPage', query: { id, type: 'reapply' } }, | |
263 | + 'bsrpRportInsert': { name: 'BsrpRportInsertPage', query: this.$isEmpty(id) ? {} : { id } }, | |
264 | + 'bsrpRportReapply': { name: 'BsrpRportInsertPage', query: { id, type: 'reapply' } }, | |
265 | + }; | |
266 | + | |
267 | + const route = routeMap[type]; | |
268 | + if (route) { | |
269 | + this.$router.push(route); | |
270 | + } else { | |
271 | + alert("올바르지 않은 경로입니다."); | |
272 | + this.$router.push(routeMap['list']); | |
273 | + } | |
274 | + }, | |
275 | + | |
276 | + // 반려 사유 검색 유틸리티 | |
277 | + getRejectionReason(sanctnList) { | |
278 | + const rejectedItem = sanctnList.find(item => item.confmAt === 'R'); | |
279 | + return rejectedItem?.returnResn; | |
280 | + }, | |
281 | + | |
282 | + // 여비 주체명 검색 유틸리티 | |
283 | + getPaymentEntityText(se, id) { | |
284 | + if (se === 'PERSONAL') { | |
285 | + const user = this.users.find(user => user.userId === id); | |
286 | + return user?.userNm; | |
287 | + } else { | |
288 | + const card = this.cards.find(card => card.cardId === id); | |
289 | + return card?.cardNm; | |
290 | + } | |
291 | + }, | |
292 | + | |
293 | + // 금액 표기 변경 유틸리티 | |
294 | + formatAmount(amount) { | |
295 | + return new Intl.NumberFormat('ko-KR').format(amount) + '원'; | |
296 | + }, | |
297 | + | |
298 | + // 파일명 검색 유틸리티 | |
299 | + getFileNm(fileOrdr) { | |
300 | + const file = this.bsrpRport.fileList.find(file => file.ordr == fileOrdr); | |
301 | + return file?.fileNm; | |
302 | + }, | |
303 | + }, | |
304 | +}; | |
305 | +</script>(파일 끝에 줄바꿈 문자 없음) |
--- client/views/pages/AppRouter.js
+++ client/views/pages/AppRouter.js
... | ... | @@ -10,17 +10,16 @@ |
10 | 10 |
import PendingApprovalListComp from '../pages/Manager/sanctn/PendingApprovalList.vue'; |
11 | 11 |
|
12 | 12 |
//근태관리 |
13 |
-import ChuljangBokmyeongDetail from '../pages/Manager/attendance/ChuljangBokmyeongDetail.vue'; |
|
14 |
-import ChuljangDetailAll from './Manager/attendance/ChuljangDetailAll.vue'; |
|
15 |
-import ChuljangStatue from '../pages/Manager/attendance/ChuljangStatue.vue'; |
|
16 |
-import ChuljangInsert from '../pages/Manager/attendance/ChuljangInsert.vue'; |
|
17 | 13 |
import myAttendance from '../pages/Manager/attendance/myAttendance.vue'; |
18 | 14 |
import buseoAttendance from '../pages/Manager/attendance/buseoAttendance.vue'; |
19 | 15 |
import AttendanceDetail from '../pages/Manager/attendance/AttendanceDetail.vue'; |
20 | 16 |
import hyugaStatue from '../pages/Manager/attendance/hyugaStatue.vue'; |
21 | 17 |
import HyugaInsert from '../pages/Manager/attendance/HyugaInsert.vue'; |
22 | 18 |
import HyugaDetail from '../pages/Manager/attendance/HyugaDetail.vue'; |
23 |
-import BokmyeongInsert from '../pages/Manager/attendance/BokmyeongInsert.vue'; |
|
19 |
+import BsrpListComp from '../pages/Manager/attendance/BsrpList.vue'; |
|
20 |
+import BsrpInsertComp from '../pages/Manager/attendance/BsrpInsert.vue'; |
|
21 |
+import BsrpViewComp from './Manager/attendance/BsrpView.vue'; |
|
22 |
+import BsrpRportInsertComp from '../pages/Manager/attendance/BsrpRportInsert.vue'; |
|
24 | 23 |
|
25 | 24 |
//업무관리 |
26 | 25 |
import projectStatue from '../pages/Manager/task/projectStatue.vue'; |
... | ... | @@ -82,11 +81,11 @@ |
82 | 81 |
{ path: 'hyugaStatue.page', name: 'hyugaStatue', component: hyugaStatue }, |
83 | 82 |
{ path: 'HyugaDetail.page', name: 'HyugaDetail', component: HyugaDetail }, |
84 | 83 |
{ path: 'hyugaInsert.page', name: 'hyugaInsert', component: HyugaInsert }, |
85 |
- { path: 'ChuljangStatue.page', name: 'ChuljangStatue', component: ChuljangStatue }, |
|
86 |
- { path: 'BokmyeongInsert.page', name: 'BokmyeongInsert', component: BokmyeongInsert }, |
|
87 |
- { path: 'ChuljangInsert.page', name: 'ChuljangInsert', component: ChuljangInsert }, |
|
88 |
- { path: 'ChuljangBokmyeongDetail.page', name: 'ChuljangBokmyeongDetail', component: ChuljangBokmyeongDetail }, |
|
89 |
- { path: 'ChuljangDetailAll.page', name: 'ChuljangDetailAll', component: ChuljangDetailAll }, |
|
84 |
+ |
|
85 |
+ { path: 'BsrpList.page', name: 'BsrpListPage', component: BsrpListComp }, |
|
86 |
+ { path: 'BsrpView.page', name: 'BsrpViewPage', component: BsrpViewComp }, |
|
87 |
+ { path: 'BsrpInsert.page', name: 'BsrpInsertPage', component: BsrpInsertComp }, |
|
88 |
+ { path: 'BsrpRportInsert.page', name: 'BsrpRportInsertPage', component: BsrpRportInsertComp }, |
|
90 | 89 |
] |
91 | 90 |
}, |
92 | 91 |
// 업무관리 |
--- client/views/pages/Manager/attendance/BokmyeongInsert.vue
... | ... | @@ -1,271 +0,0 @@ |
1 | -<template> | |
2 | - <div class="card"> | |
3 | - <div class="card-body"> | |
4 | - <h2 class="card-title">출장 복명서 등록</h2> | |
5 | - <!-- Multi Columns Form --> | |
6 | - <form class="row g-3 pt-3 needs-validation detail" @submit.prevent="handleSubmit"> | |
7 | - | |
8 | - | |
9 | - <div class="col-12"> | |
10 | - <label for="where" class="form-label">출장구분</label> | |
11 | - <input type="text" class="form-control" id="where" v-model="where" readonly/> | |
12 | - </div> | |
13 | - <div class="col-12"> | |
14 | - <label for="where" class="form-label">출장지</label> | |
15 | - <input type="text" class="form-control" id="where" v-model="where" readonly/> | |
16 | - </div> | |
17 | - <div class="col-12"> | |
18 | - <label for="where" class="form-label">출장목적</label> | |
19 | - <input type="text" class="form-control" id="where" v-model="where" readonly/> | |
20 | - </div> | |
21 | - <div class="col-12"> | |
22 | - <label for="where" class="form-label">출장기간</label> | |
23 | - <input type="text" class="form-control" id="where" v-model="where" readonly/> | |
24 | - </div> | |
25 | - | |
26 | - <div class="col-12"> | |
27 | - <label for="purpose" class="form-label">동행자</label> | |
28 | - <input type="text" class="form-control" id="purpose" v-model="purpose" readonly/> | |
29 | - </div> | |
30 | - <div class="col-12"> | |
31 | - <label for="purpose" class="form-label">법인카드</label> | |
32 | - <input type="text" class="form-control" id="purpose" v-model="purpose" readonly/> | |
33 | - </div> | |
34 | - <div class="col-12"> | |
35 | - <label for="purpose" class="form-label">법인차량</label> | |
36 | - <input type="text" class="form-control" id="purpose" v-model="purpose" readonly/> | |
37 | - </div> | |
38 | - | |
39 | - <div class="col-12"> | |
40 | - <label for="member" class="form-label"> | |
41 | - 승인자 | |
42 | - <button type="button" title="추가" @click="showPopup = true"> | |
43 | - <PlusCircleFilled /> | |
44 | - </button> | |
45 | - </label> | |
46 | - <HrPopup v-if="showPopup" @close="showPopup = false" @select="addApproval"/> | |
47 | - <div class="approval-container"> | |
48 | - <div v-for="(approval, index) in approvals" :key="index" class="d-flex gap-2 addapproval mb-2"> | |
49 | - <select class="form-select" v-model="approval.category" style="width: 110px;"> | |
50 | - <option value="결재">결재</option> | |
51 | - <option value="전결">전결</option> | |
52 | - <option value="대결">대결</option> | |
53 | - </select> | |
54 | - | |
55 | - <form class="d-flex align-items-center border-x"> | |
56 | - <input type="text" class="form-control" v-model="approval.name" style="max-width: 150px;" /> | |
57 | - <button type="button" @click="removeApproval(index)" class="delete-button"> | |
58 | - <CloseOutlined /> | |
59 | - </button> | |
60 | - </form> | |
61 | - </div> | |
62 | - </div> | |
63 | - </div> | |
64 | - <div class="col-12 chuljang"> | |
65 | - <label for="prvonsh" class="form-label">복명내용</label> | |
66 | - <input type="text" class="form-control textarea" id="reason" v-model="reason" /> | |
67 | - </div> | |
68 | - <div class="col-12 border-x"> | |
69 | - <label for="member" class="form-label"> | |
70 | - 여비계산 | |
71 | - <button type="button" title="추가" @click="addReceipt"> | |
72 | - <PlusCircleFilled /> | |
73 | - </button> | |
74 | - </label> | |
75 | - | |
76 | - <div class="approval-container"> | |
77 | - <div v-for="(receipt, index) in receipts" :key="index" class="d-flex gap-2 addapproval mb-2"> | |
78 | - <select class="form-select" style="width: 140px;"> | |
79 | - <option value="">개인결제</option> | |
80 | - <option value="">법인결제</option> | |
81 | - </select> | |
82 | - <select class="form-select" style="width: 110px;"> | |
83 | - <option value="">법인</option> | |
84 | - <option value="">개인</option> | |
85 | - </select> | |
86 | - <select class="form-select" style="width: 110px;"> | |
87 | - <option value="" selected>구분</option> | |
88 | - <option value="">여비사용</option> | |
89 | - </select> | |
90 | - | |
91 | - <input type="text" class="form-control" placeholder="금액입력" style="max-width: 150px;" /> | |
92 | - | |
93 | - <!-- 커스텀 업로드 버튼 --> | |
94 | - <label :for="'fileUpload-' + index" class="upload-label"> | |
95 | - 영수증 첨부 | |
96 | - </label> | |
97 | - | |
98 | - <!-- 실제 파일 input (숨김 처리) --> | |
99 | - <input | |
100 | - :id="'fileUpload-' + index" | |
101 | - type="file" | |
102 | - @change="handleFileUpload(index, $event)" | |
103 | - class="hidden-file-input" | |
104 | - /> | |
105 | - | |
106 | - | |
107 | - <!-- 선택된 파일 이름 표시 --> | |
108 | - <span v-if="receipt.fileName" class="file-name">{{ receipt.fileName }}</span> | |
109 | - | |
110 | - <button type="button" @click="removeReceipt(index)" class="delete-button"> | |
111 | - <CloseOutlined /> | |
112 | - </button> | |
113 | - </div> | |
114 | - </div> | |
115 | - </div> | |
116 | - | |
117 | - </form> | |
118 | - <div class="buttons"> | |
119 | - <button type="submit" class="btn sm primary">등록</button> | |
120 | - <button type="reset" class="btn sm secondary">취소</button> | |
121 | - </div> | |
122 | - | |
123 | - </div> | |
124 | - </div> | |
125 | -</template> | |
126 | - | |
127 | -<script> | |
128 | -import { PlusCircleFilled, CloseOutlined } from '@ant-design/icons-vue'; | |
129 | -import HrPopup from '../../../component/Popup/HrPopup.vue'; | |
130 | -export default { | |
131 | - data() { | |
132 | - const today = new Date().toISOString().split('T')[0]; | |
133 | - return { | |
134 | - showPopup: false, | |
135 | - fileName: '', | |
136 | - startDate: today, | |
137 | - startTime: '09:00', | |
138 | - endDate: today, | |
139 | - endTime: '18:00', | |
140 | - where: '', | |
141 | - purpose: '', | |
142 | - approvals: [ | |
143 | - ], | |
144 | - receipts: [ | |
145 | - { | |
146 | - type: '개인결제', | |
147 | - category: '결재', | |
148 | - category1: '구분', | |
149 | - }, | |
150 | - ], | |
151 | - }; | |
152 | - }, | |
153 | - components: { | |
154 | - PlusCircleFilled, CloseOutlined,HrPopup | |
155 | - }, | |
156 | - computed: { | |
157 | - loginUser() { | |
158 | - const authStore = useAuthStore(); | |
159 | - return authStore.getLoginUser; | |
160 | - }, | |
161 | - }, | |
162 | - | |
163 | - methods: { | |
164 | - handleFileUpload(index, event) { | |
165 | - const file = event.target.files[0]; | |
166 | - if (file) { | |
167 | - this.receipts[index].file = file; | |
168 | - this.receipts[index].fileName = file.name; | |
169 | - } | |
170 | - }, | |
171 | - addApproval(selectedUser) { | |
172 | - this.approvals.push({ | |
173 | - category: '결재', | |
174 | - name: selectedUser.name, // or other fields if needed | |
175 | - }); | |
176 | - this.showPopup = false; // 팝업 닫기 | |
177 | - }, | |
178 | - addReceipt() { | |
179 | - this.receipts.push({ | |
180 | - type: '개인결제', | |
181 | - category: '', | |
182 | - category1: '', | |
183 | - name: '', | |
184 | - file: null, | |
185 | - fileName: '', | |
186 | - }); | |
187 | - }, | |
188 | - // 승인자 삭제 | |
189 | - removeApproval(index) { | |
190 | - this.approvals.splice(index, 1); | |
191 | - }, | |
192 | - removeReceipt(index) { | |
193 | - this.receipts.splice(index, 1); | |
194 | -}, | |
195 | - validateForm() { | |
196 | - // 필수 입력 필드 체크 | |
197 | - if ( | |
198 | - this.startDate && | |
199 | - this.startTime && | |
200 | - this.endDate && | |
201 | - this.endTime && | |
202 | - this.where && | |
203 | - this.purpose.trim() !== "" | |
204 | - ) { | |
205 | - this.isFormValid = true; | |
206 | - } else { | |
207 | - this.isFormValid = false; | |
208 | - } | |
209 | - }, | |
210 | - calculateDayCount() { | |
211 | - const start = new Date(`${this.startDate}T${this.startTime}:00`); | |
212 | - const end = new Date(`${this.endDate}T${this.endTime}:00`); | |
213 | - | |
214 | - let totalHours = (end - start) / (1000 * 60 * 60); // 밀리초를 시간 단위로 변환 | |
215 | - | |
216 | - if (this.startDate !== this.endDate) { | |
217 | - // 시작일과 종료일이 다른경우 | |
218 | - const startDateObj = new Date(this.startDate); | |
219 | - const endDateObj = new Date(this.endDate); | |
220 | - const daysDifference = (endDateObj - startDateObj) / (1000 * 60 * 60 * 24); // 두 날짜 사이의 차이를 일수로 계산 | |
221 | - if (this.startTime !== "09:00" || this.endTime !== "18:00") { | |
222 | - this.dayCount = daysDifference + 0.5; // 시간 조건이 기준에서 벗어날 경우 | |
223 | - } else { | |
224 | - this.dayCount = Math.ceil(daysDifference + 1); // 시간 조건이 기준에 맞을 경우 | |
225 | - } | |
226 | - } else { | |
227 | - // 시작일과 종료일이 같은 경우 | |
228 | - if (this.startTime !== "09:00" || this.endTime !== "18:00") { | |
229 | - this.dayCount = 0.5; // 시작 시간 또는 종료 시간이 기준과 다를 경우 0.5 | |
230 | - } else { | |
231 | - this.dayCount = 1; // 기준 시간(09:00~18:00)이 맞으면 1일로 간주 | |
232 | - } | |
233 | - } | |
234 | - | |
235 | - this.validateForm(); // dayCount 변경 후 폼 재검증 | |
236 | - }, | |
237 | - handleSubmit() { | |
238 | - this.validateForm(); // 제출 시 유효성 확인 | |
239 | - if (this.isFormValid) { | |
240 | - localStorage.setItem('ChuljangFormData', JSON.stringify(this.$data)); | |
241 | - alert("승인 요청이 완료되었습니다."); | |
242 | - // 추가 처리 로직 (API 요청 등) | |
243 | - } else { | |
244 | - alert("모든 필드를 올바르게 작성해주세요."); | |
245 | - } | |
246 | - }, | |
247 | - loadFormData() { | |
248 | - const savedData = localStorage.getItem('ChuljangFormData'); | |
249 | - if (savedData) { | |
250 | - this.$data = JSON.parse(savedData); | |
251 | - } | |
252 | - }, | |
253 | - }, | |
254 | - mounted() { | |
255 | - // Load the saved form data when the page is loaded | |
256 | - this.loadFormData(); | |
257 | - }, | |
258 | - watch: { | |
259 | - startDate: 'calculateDayCount', | |
260 | - startTime: 'calculateDayCount', | |
261 | - endDate: 'calculateDayCount', | |
262 | - endTime: 'calculateDayCount', | |
263 | - where: 'validateForm', | |
264 | - purpose: "validateForm", | |
265 | - }, | |
266 | -}; | |
267 | -</script> | |
268 | - | |
269 | -<style scoped> | |
270 | -/* 필요한 스타일 추가 */ | |
271 | -</style> |
+++ client/views/pages/Manager/attendance/BsrpInsert.vue
... | ... | @@ -0,0 +1,583 @@ |
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>출장구분 *</th> | |
11 | + <td> | |
12 | + <select class="form-select sm" style="width: 110px;" v-model="bsrpInfo.bsrpSe"> | |
13 | + <option v-for="(item, idx) of bsrpCodes" :key="idx" :value="item.code"> {{ item.codeNm }} </option> | |
14 | + </select> | |
15 | + </td> | |
16 | + <th>일수</th> | |
17 | + <td> | |
18 | + <input type="text" class="form-control sm" v-model="totalDays" readonly /> | |
19 | + </td> | |
20 | + </tr> | |
21 | + <tr> | |
22 | + <th>출장지 *</th> | |
23 | + <td> | |
24 | + <input type="text" class="form-control sm" v-model="bsrpInfo.bsrpPlace" /> | |
25 | + </td> | |
26 | + <th>출장목적 *</th> | |
27 | + <td> | |
28 | + <input type="text" class="form-control sm" v-model="bsrpInfo.bsrpPurps" /> | |
29 | + </td> | |
30 | + </tr> | |
31 | + <tr> | |
32 | + <th>출장기간 *</th> | |
33 | + <td colspan="3"> | |
34 | + <div class="d-flex"> | |
35 | + <div class="d-flex gap-1 mb-1"> | |
36 | + <input type="date" class="form-control sm" v-model="bsrpInfo.bgnde" /> | |
37 | + <input type="text" class="form-control sm" style="width: 100px;" placeholder="시" v-model="bsrpInfo.beginHour" maxlength="2" @input="validateHour('beginHour', $event)" /> | |
38 | + <input type="text" class="form-control sm" style="width: 100px;" placeholder="분" v-model="bsrpInfo.beginMnt" maxlength="2" @input="validateMinute('beginMnt', $event)" /> | |
39 | + </div> | |
40 | + <div class="d-flex gap-1"> | |
41 | + <input type="date" class="form-control sm" v-model="bsrpInfo.endde" /> | |
42 | + <input type="text" class="form-control sm" style="width: 100px;" placeholder="시" v-model="bsrpInfo.endHour" maxlength="2" @input="validateHour('endHour', $event)" /> | |
43 | + <input type="text" class="form-control sm" style="width: 100px;" placeholder="분" v-model="bsrpInfo.endMnt" maxlength="2" @input="validateMinute('endMnt', $event)" /> | |
44 | + </div> | |
45 | + </div> | |
46 | + </td> | |
47 | + </tr> | |
48 | + <tr> | |
49 | + <th>동행자</th> | |
50 | + <td> | |
51 | + <button type="button" title="추가" @click="isOpenNmprModal = true"> | |
52 | + <PlusCircleFilled /> | |
53 | + </button> | |
54 | + <HrPopup v-if="isOpenNmprModal" :selectedEmployees="bsrpInfo.bsrpNmprList" idField="triperId" :dateInfo="bsrpInfo" @select="handleCompanionAdd" @close="isOpenNmprModal = false" /> | |
55 | + <div class="approval-container"> | |
56 | + <div v-for="(item, idx) of bsrpInfo.bsrpNmprList" :key="idx" class="d-flex addapproval"> | |
57 | + <div class="d-flex align-items-center border-x"> | |
58 | + <p>{{ item.triperNm }} {{ item.clsfNm }}</p> | |
59 | + <button type="button" @click="handleCompanionRemove(idx)" @mousedown.stop> | |
60 | + <CloseOutlined /> | |
61 | + </button> | |
62 | + </div> | |
63 | + </div> | |
64 | + </div> | |
65 | + </td> | |
66 | + <th>승인자 *</th> | |
67 | + <td> | |
68 | + <button type="button" title="추가" @click="isOpenSanctnModal = true"> | |
69 | + <PlusCircleFilled /> | |
70 | + </button> | |
71 | + <HrPopup v-if="isOpenSanctnModal" :selectedEmployees="bsrpCnsul.sanctnList" idField="confmerId" @select="handleApproverAdd" @close="isOpenSanctnModal = false" /> | |
72 | + <div class="approval-container"> | |
73 | + <SanctnList v-model:lists="bsrpCnsul.sanctnList" @delSanctn="handleApproverRemove" /> | |
74 | + </div> | |
75 | + </td> | |
76 | + </tr> | |
77 | + <tr> | |
78 | + <th>품의내용 *</th> | |
79 | + <td colspan="3" style="height: calc(100% - 550px);"> | |
80 | + <EditorComponent v-model:contents="bsrpCnsul.cn" /> | |
81 | + </td> | |
82 | + </tr> | |
83 | + <tr> | |
84 | + <th>법인카드</th> | |
85 | + <td> | |
86 | + <button type="button" title="추가" @click="isOpenCardModal = true"> | |
87 | + <PlusCircleFilled /> | |
88 | + </button> | |
89 | + <CorpCardPopup v-if="isOpenCardModal" :bsrpInfo="bsrpInfo" :lists="cards" @close="isOpenCardModal = false" @onSelected="handleCardAdd" /> | |
90 | + <div class="approval-container"> | |
91 | + <div v-for="(card, idx) in cards" :key="idx" class="d-flex gap-2 addapproval mb-2"> | |
92 | + <form class="d-flex align-items-center border-x"> | |
93 | + <p>{{ card.cardNm }}</p> | |
94 | + <button type="button" @click="handleCardRemove(idx)" class="delete-button"> | |
95 | + <CloseOutlined /> | |
96 | + </button> | |
97 | + </form> | |
98 | + </div> | |
99 | + </div> | |
100 | + </td> | |
101 | + <th>법인차량</th> | |
102 | + <td> | |
103 | + <button type="button" title="추가" @click="isOpenVhcleModal = true"> | |
104 | + <PlusCircleFilled /> | |
105 | + </button> | |
106 | + <CorpCarPopup v-if="isOpenVhcleModal" :bsrpInfo="bsrpInfo" :lists="vhcles" @close="isOpenVhcleModal = false" @onSelected="handleVehicleAdd" /> | |
107 | + <div class="approval-container"> | |
108 | + <div v-for="(vhcle, idx) in vhcles" :key="idx" class="d-flex gap-2 addapproval mb-2"> | |
109 | + <p>{{ vhcle.vhcleNm }}</p> | |
110 | + <select class="form-select" v-model="vhcle.drverId"> | |
111 | + <option value="" disabled hidden>운전자 선택</option> | |
112 | + <option :value="userInfo.userId">{{ userInfo.userNm }}</option> | |
113 | + <option v-for="(item, idx) of bsrpInfo.bsrpNmprList" :key="idx" :value="item.userId"> {{ item.userNm }} </option> | |
114 | + </select> | |
115 | + <button type="button" @click="handleVehicleRemove(idx)" class="delete-button"> | |
116 | + <CloseOutlined /> | |
117 | + </button> | |
118 | + </div> | |
119 | + </div> | |
120 | + </td> | |
121 | + </tr> | |
122 | + </tbody> | |
123 | + </table> | |
124 | + </div> | |
125 | + <div class="buttons"> | |
126 | + <button type="button" class="btn sm btn-red" @click="handleLoadLastApprovers">이전 승인자 불러오기</button> | |
127 | + <button type="button" class="btn sm primary" v-if="$isEmpty(pageId)" @click="handleSave">신청</button> | |
128 | + <template v-else> | |
129 | + <button type="button" class="btn sm primary" @click="handleUpdate"> {{ submitButtonText }} </button> | |
130 | + <button type="button" class="btn sm secondary" @click="handleCancel">취소</button> | |
131 | + </template> | |
132 | + </div> | |
133 | + </div> | |
134 | + </div> | |
135 | +</template> | |
136 | +<script> | |
137 | +import { PlusCircleFilled, CloseOutlined } from '@ant-design/icons-vue'; | |
138 | +import HrPopup from '../../../component/Popup/HrPopup.vue'; | |
139 | +import CorpCarPopup from '../../../component/Popup/CorpCarPopup.vue'; | |
140 | +import CorpCardPopup from '../../../component/Popup/CorpCardPopup.vue'; | |
141 | +import SanctnList from '../../../component/Sanctn/SanctnFormList.vue'; | |
142 | +import EditorComponent from '../../../component/editor/EditorComponent.vue'; | |
143 | +// API | |
144 | +import { saveBsrpProc, findBsrpProc, updateBsrpProc, findLastBsrpCnsulProc } from '../../../../resources/api/bsrp'; | |
145 | + | |
146 | +export default { | |
147 | + name: 'BsrpInsert', | |
148 | + | |
149 | + components: { | |
150 | + PlusCircleFilled, CloseOutlined, | |
151 | + HrPopup, CorpCarPopup, CorpCardPopup, SanctnList, EditorComponent, | |
152 | + }, | |
153 | + | |
154 | + data() { | |
155 | + return { | |
156 | + pageId: null, | |
157 | + pageMode: null, | |
158 | + userInfo: this.$store.state.userInfo, | |
159 | + isOpenNmprModal: false, | |
160 | + isOpenSanctnModal: false, | |
161 | + isOpenCardModal: false, | |
162 | + isOpenVhcleModal: false, | |
163 | + bsrpCodes: [], | |
164 | + sanctnCodes: [], | |
165 | + defaultSanctnCode: null, | |
166 | + bsrpInfo: { | |
167 | + bsrpId: null, | |
168 | + applcntId: null, | |
169 | + bsrpSe: null, | |
170 | + bsrpSeNm: null, | |
171 | + bsrpPlace: null, | |
172 | + bsrpPurps: null, | |
173 | + bgnde: null, | |
174 | + beginHour: null, | |
175 | + beginMnt: null, | |
176 | + endde: null, | |
177 | + endHour: null, | |
178 | + endMnt: null, | |
179 | + bsrpNmprList: [] | |
180 | + }, | |
181 | + cards: [], | |
182 | + vhcles: [], | |
183 | + bsrpCnsul: { | |
184 | + bsrpId: null, | |
185 | + cn: null, | |
186 | + rgsde: null, | |
187 | + register: null, | |
188 | + updde: null, | |
189 | + updusr: null, | |
190 | + sanctnList: [] | |
191 | + }, | |
192 | + totalDays: 0, | |
193 | + }; | |
194 | + }, | |
195 | + | |
196 | + watch: { | |
197 | + // 시작일 변동 시 날짜 재계산 | |
198 | + 'bsrpInfo.bgnde'() { | |
199 | + this.calculateDays(); | |
200 | + }, | |
201 | + // 종료일 변동 시 날짜 재계산 | |
202 | + 'bsrpInfo.endde'() { | |
203 | + this.calculateDays(); | |
204 | + }, | |
205 | + }, | |
206 | + | |
207 | + computed: { | |
208 | + // 재신청 여부 | |
209 | + isReapplyMode() { | |
210 | + return this.pageMode === 'reapply'; | |
211 | + }, | |
212 | + | |
213 | + // 재신청 여부에 따른 등록 버튼 변경 | |
214 | + submitButtonText() { | |
215 | + return this.isReapplyMode ? '재신청' : '수정'; | |
216 | + } | |
217 | + }, | |
218 | + | |
219 | + async created() { | |
220 | + this.pageId = this.$route.query.id; | |
221 | + this.pageMode = this.$route.query.type; | |
222 | + | |
223 | + this.bsrpCodes = await this.$findChildCodes('sanctn_mby_bsrp'); // 출장 구분 코드 조회 | |
224 | + this.bsrpInfo.bsrpSe = this.bsrpCodes[0].code; | |
225 | + | |
226 | + this.sanctnCodes = await this.$findChildCodes('sanctn_code'); // 결재 구분 코드 조회 | |
227 | + this.defaultSanctnCode = this.sanctnCodes[0].code; | |
228 | + }, | |
229 | + | |
230 | + mounted() { | |
231 | + if (!this.$isEmpty(this.pageId)) { | |
232 | + this.fetchData(); // 출장 정보 조회 | |
233 | + } | |
234 | + }, | |
235 | + | |
236 | + methods: { | |
237 | + // 출장 정보 조회 | |
238 | + async fetchData() { | |
239 | + try { | |
240 | + const response = await findBsrpProc(this.pageId); | |
241 | + const result = response.data.data; | |
242 | + | |
243 | + this.bsrpInfo = result.bsrpInfoDTO; | |
244 | + this.cards = result.cards; | |
245 | + this.vhcles = result.vhcles; | |
246 | + this.bsrpCnsul = result.bsrpCnsulDTO; | |
247 | + } catch (error) { | |
248 | + alert(error.response.data.message); | |
249 | + this.handleNavigation('list'); | |
250 | + } | |
251 | + }, | |
252 | + | |
253 | + // 이전 승인자 조회 핸들러 | |
254 | + async handleLoadLastApprovers() { | |
255 | + try { | |
256 | + const response = await findLastBsrpCnsulProc(); | |
257 | + const result = response.data.data; | |
258 | + | |
259 | + this.bsrpCnsul.sanctnList = result; | |
260 | + } catch (error) { | |
261 | + const message = error.response?.data?.message || "이전 승인자를 불러오는데 실패했습니다."; | |
262 | + alert(message); | |
263 | + } | |
264 | + }, | |
265 | + | |
266 | + // 저장 핸들러 | |
267 | + async handleSave() { | |
268 | + try { | |
269 | + if (!this.validateForm()) { | |
270 | + return; | |
271 | + } | |
272 | + | |
273 | + let data = this.buildSendData("insert"); | |
274 | + | |
275 | + const response = await saveBsrpProc(data); | |
276 | + alert("등록되었습니다."); | |
277 | + | |
278 | + this.handleNavigation('view', response.data.data.pageId); | |
279 | + } catch (error) { | |
280 | + this.handleError(error); | |
281 | + } | |
282 | + }, | |
283 | + | |
284 | + // 수정 핸들러 | |
285 | + async handleUpdate() { | |
286 | + try { | |
287 | + if (!this.validateForm()) { | |
288 | + return; | |
289 | + } | |
290 | + | |
291 | + let data = this.buildSendData("update"); | |
292 | + | |
293 | + const response = await updateBsrpProc(this.pageId, data); | |
294 | + const message = this.isReapplyMode ? "재신청되었습니다." : "수정되었습니다."; | |
295 | + alert(message); | |
296 | + | |
297 | + this.handleNavigation('view', response.data.data.pageId); | |
298 | + } catch (error) { | |
299 | + this.handleError(error); | |
300 | + } | |
301 | + }, | |
302 | + | |
303 | + // 동행자 추가 핸들러 | |
304 | + handleCompanionAdd(item) { | |
305 | + const data = { | |
306 | + triperId: item.userId, | |
307 | + triperNm: item.userNm, | |
308 | + deptId: item.deptId, | |
309 | + deptNm: item.deptNm, | |
310 | + clsf: item.clsf, | |
311 | + clsfNm: item.clsfNm, | |
312 | + }; | |
313 | + | |
314 | + this.bsrpInfo.bsrpNmprList.push(data); | |
315 | + this.isOpenNmprModal = false; | |
316 | + }, | |
317 | + | |
318 | + // 동행자 삭제 핸들러 | |
319 | + handleCompanionRemove(idx) { | |
320 | + this.bsrpInfo.bsrpNmprList.splice(idx, 1); | |
321 | + }, | |
322 | + | |
323 | + // 승인자 추가 핸들러 | |
324 | + handleApproverAdd(user) { | |
325 | + const data = { | |
326 | + confmerId: user.userId, | |
327 | + confmerNm: user.userNm, | |
328 | + clsf: user.clsf, | |
329 | + clsfNm: user.clsfNm, | |
330 | + sanctnOrdr: this.bsrpCnsul.sanctnList.length + 1, | |
331 | + sanctnIem: 'bsrp_cnsul', | |
332 | + sanctnSe: this.defaultSanctnCode, | |
333 | + }; | |
334 | + | |
335 | + this.bsrpCnsul.sanctnList.push(data); | |
336 | + this.isOpenSanctnModal = false; | |
337 | + }, | |
338 | + | |
339 | + // 승인자 삭제 핸들러 | |
340 | + handleApproverRemove(idx) { | |
341 | + this.bsrpCnsul.sanctnList.splice(idx, 1); | |
342 | + this.bsrpCnsul.sanctnList.forEach((item, index) => { | |
343 | + item.sanctnOrdr = index + 1; | |
344 | + }); | |
345 | + }, | |
346 | + | |
347 | + // 법인 카드 추가 핸들러 | |
348 | + handleCardAdd(item) { | |
349 | + this.cards.push(item); | |
350 | + this.isOpenCardModal = false; | |
351 | + }, | |
352 | + | |
353 | + // 법인 카드 삭제 핸들러 | |
354 | + handleCardRemove(idx) { | |
355 | + this.cards.splice(idx, 1); | |
356 | + }, | |
357 | + | |
358 | + // 법인 차량 추가 핸들러 | |
359 | + handleVehicleAdd(item) { | |
360 | + item.drverId = this.userInfo.userId; | |
361 | + this.vhcles.push(item); | |
362 | + this.isOpenVhcleModal = false; | |
363 | + }, | |
364 | + | |
365 | + // 법인 차량 삭제 핸들러 | |
366 | + handleVehicleRemove(idx) { | |
367 | + this.vhcles.splice(idx, 1); | |
368 | + }, | |
369 | + | |
370 | + // 취소 핸들러 | |
371 | + handleCancel() { | |
372 | + if (confirm('작성 중인 내용이 삭제됩니다. 계속하시겠습니까?')) { | |
373 | + this.handleNavigation('view', this.pageId); | |
374 | + } | |
375 | + }, | |
376 | + | |
377 | + // 페이지 이동 핸들러 | |
378 | + handleNavigation(type, id) { | |
379 | + const routeMap = { | |
380 | + 'list': { name: 'BsrpListPage' }, | |
381 | + 'view': { name: 'BsrpViewPage', query: { id } }, | |
382 | + 'bsrpInsert': { name: 'BsrpInsertPage', query: this.$isEmpty(id) ? {} : { id } }, | |
383 | + 'bsrpReapply': { name: 'BsrpInsertPage', query: { id, type: 'reapply' } }, | |
384 | + 'bsrpRportInsert': { name: 'BsrpRportInsertPage', query: this.$isEmpty(id) ? {} : { id } }, | |
385 | + 'bsrpRportReapply': { name: 'BsrpRportInsertPage', query: { id, type: 'reapply' } }, | |
386 | + }; | |
387 | + | |
388 | + const route = routeMap[type]; | |
389 | + if (route) { | |
390 | + this.$router.push(route); | |
391 | + } else { | |
392 | + alert("올바르지 않은 경로를 요청하여 목록으로 이동합니다."); | |
393 | + this.$router.push({ name: 'BsrpListPage' }); | |
394 | + } | |
395 | + }, | |
396 | + | |
397 | + // 에러 핸들러 | |
398 | + handleError(error) { | |
399 | + const message = error.response?.data?.message || "에러가 발생했습니다."; | |
400 | + alert(message); | |
401 | + }, | |
402 | + | |
403 | + // 일수 계산 유틸리티 | |
404 | + calculateDays() { | |
405 | + if (!this.bsrpInfo.bgnde || !this.bsrpInfo.endde) { | |
406 | + this.totalDays = 0; | |
407 | + return; | |
408 | + } | |
409 | + | |
410 | + const startDate = new Date(this.bsrpInfo.bgnde); | |
411 | + const endDate = new Date(this.bsrpInfo.endde); | |
412 | + | |
413 | + if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { | |
414 | + this.totalDays = 0; | |
415 | + return; | |
416 | + } | |
417 | + | |
418 | + const timeDiff = endDate.getTime() - startDate.getTime(); | |
419 | + const dayDiff = Math.floor(timeDiff / (1000 * 60 * 60 * 24)) + 1; | |
420 | + | |
421 | + this.totalDays = Math.max(0, dayDiff); | |
422 | + }, | |
423 | + | |
424 | + // 출장 시간 유효성 검사 유틸리티 | |
425 | + validateHour(field, event) { | |
426 | + let value = event.target.value.replace(/[^0-9]/g, ''); | |
427 | + | |
428 | + if (value.length > 0) { | |
429 | + const hour = parseInt(value); | |
430 | + if (hour > 23) { | |
431 | + value = '23'; | |
432 | + } | |
433 | + } | |
434 | + | |
435 | + this.bsrpInfo[field] = value; | |
436 | + }, | |
437 | + | |
438 | + // 출장 분 유효성 검사 유틸리티 | |
439 | + validateMinute(field, event) { | |
440 | + let value = event.target.value.replace(/[^0-9]/g, ''); | |
441 | + | |
442 | + if (value.length > 0) { | |
443 | + const minute = parseInt(value); | |
444 | + if (minute > 59) { | |
445 | + value = '59'; | |
446 | + } | |
447 | + } | |
448 | + | |
449 | + this.bsrpInfo[field] = value; | |
450 | + }, | |
451 | + | |
452 | + // 입력값 전체 유효성 검사 유틸리티 | |
453 | + validateForm() { | |
454 | + if (this.$isEmpty(this.bsrpInfo.bsrpSe)) { | |
455 | + alert('출장구분을 선택해주세요.'); | |
456 | + return false; | |
457 | + } | |
458 | + | |
459 | + if (this.$isEmpty(this.bsrpInfo.bsrpPlace)) { | |
460 | + alert('출장지를 입력해주세요.'); | |
461 | + return false; | |
462 | + } | |
463 | + | |
464 | + if (this.$isEmpty(this.bsrpInfo.bsrpPurps)) { | |
465 | + alert('출장목적을 입력해주세요.'); | |
466 | + return false; | |
467 | + } | |
468 | + | |
469 | + if (this.$isEmpty(this.bsrpInfo.bgnde) || this.$isEmpty(this.bsrpInfo.beginHour) || this.$isEmpty(this.bsrpInfo.beginMnt)) { | |
470 | + alert('출장 시작일시를 모두 입력해주세요.'); | |
471 | + return false; | |
472 | + } | |
473 | + | |
474 | + if (this.$isEmpty(this.bsrpInfo.endde) || this.$isEmpty(this.bsrpInfo.endHour) || this.$isEmpty(this.bsrpInfo.endMnt)) { | |
475 | + alert('출장 종료일시를 모두 입력해주세요.'); | |
476 | + return false; | |
477 | + } | |
478 | + | |
479 | + if (!this.validateTimeRange(this.bsrpInfo.beginHour, this.bsrpInfo.beginMnt, '시작')) { | |
480 | + return false; | |
481 | + } | |
482 | + | |
483 | + if (!this.validateTimeRange(this.bsrpInfo.endHour, this.bsrpInfo.endMnt, '종료')) { | |
484 | + return false; | |
485 | + } | |
486 | + | |
487 | + if (this.$isEmpty(this.bsrpCnsul.sanctnList) || this.bsrpCnsul.sanctnList.length === 0) { | |
488 | + alert('승인자를 선택해주세요.'); | |
489 | + return false; | |
490 | + } | |
491 | + | |
492 | + if (this.$isEmpty(this.bsrpCnsul.cn)) { | |
493 | + alert('품의내용을 입력해주세요.'); | |
494 | + return false; | |
495 | + } | |
496 | + | |
497 | + return true; | |
498 | + }, | |
499 | + | |
500 | + // 출장 기간 유효성 검사 유틸리티 | |
501 | + validateTimeRange(hour, minute, timeType) { | |
502 | + const hourNum = parseInt(hour); | |
503 | + const minuteNum = parseInt(minute); | |
504 | + | |
505 | + if (isNaN(hourNum) || hourNum < 0 || hourNum > 23) { | |
506 | + alert(`${timeType} 시간은 0-23 사이의 값을 입력해주세요.`); | |
507 | + return false; | |
508 | + } | |
509 | + | |
510 | + if (isNaN(minuteNum) || minuteNum < 0 || minuteNum > 59) { | |
511 | + alert(`${timeType} 분은 0-59 사이의 값을 입력해주세요.`); | |
512 | + return false; | |
513 | + } | |
514 | + | |
515 | + return true; | |
516 | + }, | |
517 | + | |
518 | + // 데이터 가공 유틸리티 | |
519 | + buildSendData(type) { | |
520 | + let bsrpInfo = { | |
521 | + applcntId: this.bsrpInfo.applcntId, | |
522 | + bsrpSe: this.bsrpInfo.bsrpSe, | |
523 | + bsrpPlace: this.bsrpInfo.bsrpPlace, | |
524 | + bsrpPurps: this.bsrpInfo.bsrpPurps, | |
525 | + bgnde: this.bsrpInfo.bgnde, | |
526 | + beginHour: this.bsrpInfo.beginHour, | |
527 | + beginMnt: this.bsrpInfo.beginMnt, | |
528 | + endde: this.bsrpInfo.endde, | |
529 | + endHour: this.bsrpInfo.endHour, | |
530 | + endMnt: this.bsrpInfo.endMnt, | |
531 | + bsrpNmprList: this.bsrpInfo.bsrpNmprList, | |
532 | + } | |
533 | + | |
534 | + let cards = []; | |
535 | + for (let card of this.cards) { | |
536 | + cards.push({ | |
537 | + cardId: card.cardId, | |
538 | + }) | |
539 | + } | |
540 | + | |
541 | + let vhcles = []; | |
542 | + for (let vhcle of this.vhcles) { | |
543 | + vhcles.push({ | |
544 | + vhcleId: vhcle.vhcleId, | |
545 | + drverId: vhcle.drverId, | |
546 | + }) | |
547 | + } | |
548 | + | |
549 | + let sanctnList = []; | |
550 | + for (let sanctn of this.bsrpCnsul.sanctnList) { | |
551 | + sanctnList.push({ | |
552 | + confmerId: sanctn.confmerId, | |
553 | + clsf: sanctn.clsf, | |
554 | + sanctnOrdr: sanctn.sanctnOrdr, | |
555 | + sanctnIem: sanctn.sanctnIem, | |
556 | + sanctnSe: sanctn.sanctnSe, | |
557 | + }) | |
558 | + } | |
559 | + | |
560 | + let bsrpCnsul = { | |
561 | + cn: this.bsrpCnsul.cn, | |
562 | + sanctnList: sanctnList, | |
563 | + } | |
564 | + | |
565 | + if (type === "insert") { | |
566 | + return { | |
567 | + insertBsrpInfoDTO: bsrpInfo, | |
568 | + cardDtlsList: cards, | |
569 | + vhcleDtlsList: vhcles, | |
570 | + insertBsrpCnsulDTO: bsrpCnsul, | |
571 | + }; | |
572 | + } else if (type === "update") { | |
573 | + return { | |
574 | + updateBsrpInfoDTO: bsrpInfo, | |
575 | + cardDtlsList: cards, | |
576 | + vhcleDtlsList: vhcles, | |
577 | + updateBsrpCnsulDTO: bsrpCnsul, | |
578 | + }; | |
579 | + } | |
580 | + }, | |
581 | + }, | |
582 | +}; | |
583 | +</script>(파일 끝에 줄바꿈 문자 없음) |
+++ client/views/pages/Manager/attendance/BsrpList.vue
... | ... | @@ -0,0 +1,183 @@ |
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="sch-form-wrap"> | |
7 | + <div class="input-group"> | |
8 | + <select class="form-select" v-model="searchParams.year" @change="handleSearchChange"> | |
9 | + <option value="">연도 전체</option> | |
10 | + <option v-for="year in yearOptions" :key="year" :value="year">{{ year }}년</option> | |
11 | + </select> | |
12 | + <select class="form-select" v-model="searchParams.month" @change="handleSearchChange"> | |
13 | + <option value="">월 전체</option> | |
14 | + <option v-for="month in monthOptions" :key="month" :value="month">{{ month }}월</option> | |
15 | + </select> | |
16 | + </div> | |
17 | + </div> | |
18 | + <div class="tbl-wrap"> | |
19 | + <table id="myTable" class="tbl data"> | |
20 | + <thead> | |
21 | + <tr> | |
22 | + <th>출장구분</th> | |
23 | + <th>출장지</th> | |
24 | + <th>목적</th> | |
25 | + <th>출장기간</th> | |
26 | + <th>품의서 상태</th> | |
27 | + <th>복명서 등록 여부</th> | |
28 | + <th>복명서 상태</th> | |
29 | + </tr> | |
30 | + </thead> | |
31 | + <tbody> | |
32 | + <template v-if="hasBusinessTrips"> | |
33 | + <tr v-for="(item, idx) in businessTripList" :key="`${item.bsrpId}-${idx}`" @click="handleItemClick(item.bsrpId)"> | |
34 | + <td>{{ item.bsrpSeNm }}</td> | |
35 | + <td>{{ item.bsrpPlace }}</td> | |
36 | + <td>{{ item.bsrpPurps }}</td> | |
37 | + <td>{{ $formattedDates(item) }}</td> | |
38 | + <td>{{ item.cnsulConfmAtNm }}</td> | |
39 | + <td>{{ getReportRegistrationStatus(item.hasRport) }}</td> | |
40 | + <td>{{ getReportStatus(item) }}</td> | |
41 | + </tr> | |
42 | + </template> | |
43 | + <tr v-else> | |
44 | + <td colspan="7">게시물이 존재하지 않습니다.</td> | |
45 | + </tr> | |
46 | + </tbody> | |
47 | + </table> | |
48 | + </div> | |
49 | + <Pagination :search="searchParams" @onChange="handlePageChange" /> | |
50 | + </div> | |
51 | + </div> | |
52 | + </div> | |
53 | +</template> | |
54 | +<script> | |
55 | +// API | |
56 | +import { findBsrpsProc } from '../../../../resources/api/bsrp'; | |
57 | + | |
58 | +export default { | |
59 | + name: 'BsrpList', | |
60 | + | |
61 | + // 상수 - 라우트 맵 | |
62 | + ROUTE_MAP: { | |
63 | + 'list': { name: 'BsrpListPage' }, | |
64 | + 'view': { name: 'BsrpViewPage', query: { id: null } }, | |
65 | + 'bsrpInsert': { name: 'BsrpInsertPage', query: {} }, | |
66 | + 'bsrpReapply': { name: 'BsrpInsertPage', query: { type: 'reapply' } }, | |
67 | + 'bsrpRportInsert': { name: 'BsrpRportInsertPage', query: {} }, | |
68 | + 'bsrpRportReapply': { name: 'BsrpRportInsertPage', query: { type: 'reapply' } }, | |
69 | + }, | |
70 | + | |
71 | + data() { | |
72 | + return { | |
73 | + searchParams: { | |
74 | + year: '', | |
75 | + month: '', | |
76 | + currentUserId: this.$store.state.userInfo.userId, | |
77 | + }, | |
78 | + businessTripList: [], | |
79 | + }; | |
80 | + }, | |
81 | + | |
82 | + computed: { | |
83 | + // 연도 옵션 | |
84 | + yearOptions() { | |
85 | + const startYear = 2020; | |
86 | + const currentYear = new Date().getFullYear(); | |
87 | + const years = []; | |
88 | + | |
89 | + for (let year = currentYear; year >= startYear; year--) { | |
90 | + years.push(year); | |
91 | + } | |
92 | + return years; | |
93 | + }, | |
94 | + | |
95 | + // 월 옵션 | |
96 | + monthOptions() { | |
97 | + return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; | |
98 | + }, | |
99 | + | |
100 | + // 출장 목록 유무 | |
101 | + hasBusinessTrips() { | |
102 | + return this.businessTripList.length > 0; | |
103 | + }, | |
104 | + }, | |
105 | + | |
106 | + async mounted() { | |
107 | + await this.fetchBusinessTripList(); // 출장 목록 조회 | |
108 | + }, | |
109 | + | |
110 | + methods: { | |
111 | + // 출장 목록 조회 | |
112 | + async fetchBusinessTripList() { | |
113 | + try { | |
114 | + const response = await findBsrpsProc(this.searchParams); | |
115 | + const result = response.data.data; | |
116 | + | |
117 | + this.businessTripList = result.lists; | |
118 | + this.searchParams = result.search; | |
119 | + } catch (error) { | |
120 | + this.handleError(error); | |
121 | + } | |
122 | + }, | |
123 | + | |
124 | + // 검색 핸들러 (검색 시 현재 페이지를 1로 변경 후 조회) | |
125 | + async handleSearchChange() { | |
126 | + await this.handlePageChange(1); | |
127 | + }, | |
128 | + | |
129 | + // 페이지 변경 핸들러 | |
130 | + async handlePageChange(currentPage) { | |
131 | + this.searchParams.currentPage = Number(currentPage); | |
132 | + await this.$nextTick(); | |
133 | + await this.fetchBusinessTripList(); | |
134 | + }, | |
135 | + | |
136 | + // 상세 페이지 이동 핸들러 | |
137 | + handleItemClick(id) { | |
138 | + this.handleNavigation('view', id); | |
139 | + }, | |
140 | + | |
141 | + // 페이지 이동 핸들러 | |
142 | + handleNavigation(type, id) { | |
143 | + const routeMap = { | |
144 | + 'list': { name: 'BsrpListPage' }, | |
145 | + 'view': { name: 'BsrpViewPage', query: { id } }, | |
146 | + 'bsrpInsert': { name: 'BsrpInsertPage', query: this.$isEmpty(id) ? {} : { id } }, | |
147 | + 'bsrpReapply': { name: 'BsrpInsertPage', query: { id, type: 'reapply' } }, | |
148 | + 'bsrpRportInsert': { name: 'BsrpRportInsertPage', query: this.$isEmpty(id) ? {} : { id } }, | |
149 | + 'bsrpRportReapply': { name: 'BsrpRportInsertPage', query: { id, type: 'reapply' } }, | |
150 | + }; | |
151 | + | |
152 | + const route = routeMap[type]; | |
153 | + if (route) { | |
154 | + this.$router.push(route); | |
155 | + } else { | |
156 | + alert("올바르지 않은 경로입니다."); | |
157 | + this.$router.push(routeMap['list']); | |
158 | + } | |
159 | + }, | |
160 | + | |
161 | + // 에러 핸들러 | |
162 | + handleError(error) { | |
163 | + const message = error.response?.data?.message || "에러가 발생했습니다."; | |
164 | + alert(message); | |
165 | + }, | |
166 | + | |
167 | + // 등록 여부 유틸 | |
168 | + getReportRegistrationStatus(hasRport) { | |
169 | + return hasRport ? '등록' : '미등록'; | |
170 | + }, | |
171 | + | |
172 | + // 상태 유틸 | |
173 | + getReportStatus(item) { | |
174 | + return item.hasRport ? item.rportConfmAtNm : '-'; | |
175 | + }, | |
176 | + }, | |
177 | +}; | |
178 | +</script> | |
179 | +<style scoped> | |
180 | +tr { | |
181 | + cursor: pointer; | |
182 | +} | |
183 | +</style>(파일 끝에 줄바꿈 문자 없음) |
+++ client/views/pages/Manager/attendance/BsrpRportInsert.vue
... | ... | @@ -0,0 +1,465 @@ |
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>출장구분</th> | |
11 | + <td>{{ bsrpInfo.bsrpSeNm }}</td> | |
12 | + </tr> | |
13 | + <tr> | |
14 | + <th>출장지</th> | |
15 | + <td>{{ bsrpInfo.bsrpPlace }}</td> | |
16 | + </tr> | |
17 | + <tr> | |
18 | + <th>출장목적</th> | |
19 | + <td>{{ bsrpInfo.bsrpPurps }}</td> | |
20 | + </tr> | |
21 | + <tr> | |
22 | + <th>출장기간</th> | |
23 | + <td>{{ $formattedDates(bsrpInfo) }}</td> | |
24 | + </tr> | |
25 | + <tr> | |
26 | + <th>동행자</th> | |
27 | + <td> | |
28 | + <span v-for="(item, idx) of bsrpInfo.bsrpNmprList" :key="idx"> {{ item.triperNm }}{{ idx !== bsrpInfo.bsrpNmprList.length - 1 ? ', ' : '' }} </span> | |
29 | + </td> | |
30 | + </tr> | |
31 | + <tr> | |
32 | + <th>법인카드</th> | |
33 | + <td> | |
34 | + <span v-for="(item, idx) of cards" :key="idx">{{ item.cardNm }}</span> | |
35 | + </td> | |
36 | + </tr> | |
37 | + <tr> | |
38 | + <th>법인차량</th> | |
39 | + <td> | |
40 | + <span v-for="(item, idx) of vhcles" :key="idx">{{ item.vhcleNm }}</span> | |
41 | + </td> | |
42 | + </tr> | |
43 | + <tr> | |
44 | + <th>승인자 *</th> | |
45 | + <td> | |
46 | + <button type="button" title="추가" @click="isOpenSanctnModal = true"> | |
47 | + <PlusCircleFilled /> | |
48 | + </button> | |
49 | + <HrPopup v-if="isOpenSanctnModal" :selectedEmployees="bsrpRport.sanctnList" idField="confmerId" @select="handleApproverAdd" @close="isOpenSanctnModal = false" /> | |
50 | + <div class="approval-container"> | |
51 | + <SanctnList v-model:lists="bsrpRport.sanctnList" @delSanctn="handleApproverRemove" /> | |
52 | + </div> | |
53 | + </td> | |
54 | + </tr> | |
55 | + <tr> | |
56 | + <th>복명내용 *</th> | |
57 | + <td style="height: calc(100% - 550px);"> | |
58 | + <EditorComponent v-model:contents="bsrpRport.cn" /> | |
59 | + </td> | |
60 | + </tr> | |
61 | + <tr> | |
62 | + <th>여비계산</th> | |
63 | + <td> | |
64 | + <button type="button" title="추가" @click="handleExpenseAdd"> | |
65 | + <PlusCircleFilled /> | |
66 | + </button> | |
67 | + <div v-for="(item, idx) of bsrpRport.bsrpTrvctList" :key="idx" class="d-flex gap-2 addapproval mb-2"> | |
68 | + <select class="form-select" v-model="item.se" style="width: 140px;"> | |
69 | + <option value="" disabled hidden>결제 방식</option> | |
70 | + <option v-for="code of trvctTypeCodes" :key="code.code" :value="code.code"> {{ code.codeNm }} </option> | |
71 | + </select> | |
72 | + <select class="form-select" v-model="item.setleMbyId" style="width: 140px;"> | |
73 | + <option value="" disabled hidden>결제 주체</option> | |
74 | + <template v-if="item.se === 'PERSONAL'"> | |
75 | + <option :value="userInfo.userId">{{ userInfo.userNm }}</option> | |
76 | + <option v-for="nmpr of bsrpInfo.bsrpNmprList" :key="nmpr.userId" :value="nmpr.userId"> {{ nmpr.triperNm }} </option> | |
77 | + </template> | |
78 | + <template v-else-if="item.se === 'CORPORATE'"> | |
79 | + <option v-for="card of cards" :key="card.cardId" :value="card.cardId"> {{ card.cardNm }} </option> | |
80 | + </template> | |
81 | + </select> | |
82 | + <select class="form-select" v-model="item.ty" style="width: 140px;"> | |
83 | + <option value="" disabled hidden>출장비 구분</option> | |
84 | + <option v-for="expense of travelExpenseCodes" :key="expense.code" :value="expense.code"> {{ expense.codeNm }} </option> | |
85 | + </select> | |
86 | + <input type="number" class="form-control" placeholder="금액입력" v-model="item.amount" style="max-width: 150px;" /> | |
87 | + <div> | |
88 | + <label :for="'fileUpload-' + idx" class="btn sm primary">영수증 첨부</label> | |
89 | + <input :id="'fileUpload-' + idx" type="file" @change="handleFileUpload(idx, $event)" class="hidden-file-input" accept="image/*,.pdf" /> | |
90 | + <span v-if="item.fileName" class="file-name">{{ item.fileName }}</span> | |
91 | + </div> | |
92 | + <button type="button" @click="handleExpenseRemove(idx)" class="delete-button"> | |
93 | + <CloseOutlined /> | |
94 | + </button> | |
95 | + </div> | |
96 | + </td> | |
97 | + </tr> | |
98 | + </tbody> | |
99 | + </table> | |
100 | + </div> | |
101 | + <div class="buttons"> | |
102 | + <button type="button" class="btn sm btn-red" @click="handleLoadLastApprovers">이전 승인자 불러오기</button> | |
103 | + <button type="button" class="btn sm primary" v-if="!this.hasBsrpRport" @click="handleSave">신청</button> | |
104 | + <template v-else> | |
105 | + <button type="button" class="btn sm primary" @click="handleUpdate"> {{ submitButtonText }} </button> | |
106 | + <button type="button" class="btn sm secondary" @click="handleCancel">취소</button> | |
107 | + </template> | |
108 | + </div> | |
109 | + </div> | |
110 | + </div> | |
111 | +</template> | |
112 | +<script> | |
113 | +import { PlusCircleFilled, CloseOutlined } from '@ant-design/icons-vue'; | |
114 | +import HrPopup from '../../../component/Popup/HrPopup.vue'; | |
115 | +import SanctnList from '../../../component/Sanctn/SanctnFormList.vue'; | |
116 | +import EditorComponent from '../../../component/editor/EditorComponent.vue'; | |
117 | +// API | |
118 | +import { findBsrpProc } from '../../../../resources/api/bsrp'; | |
119 | +import { findBsrpRportProc, saveBsrpRportProc, updateBsrpRport, findLastBsrpRportProc } from '../../../../resources/api/bsrpRport'; | |
120 | + | |
121 | +export default { | |
122 | + name: 'BsrpRportInsert', | |
123 | + | |
124 | + components: { | |
125 | + PlusCircleFilled, | |
126 | + CloseOutlined, | |
127 | + HrPopup, | |
128 | + SanctnList, | |
129 | + EditorComponent | |
130 | + }, | |
131 | + | |
132 | + data() { | |
133 | + return { | |
134 | + pageId: null, | |
135 | + pageMode: null, | |
136 | + userInfo: this.$store.state.userInfo, | |
137 | + isOpenSanctnModal: false, | |
138 | + sanctnCodes: [], | |
139 | + defaultSanctnCode: null, | |
140 | + trvctTypeCodes: [], | |
141 | + travelExpenseCodes: [], | |
142 | + bsrpInfo: { | |
143 | + bsrpNmprList: [] | |
144 | + }, | |
145 | + cards: [], | |
146 | + vhcles: [], | |
147 | + hasBsrpRport: false, | |
148 | + bsrpRport: { | |
149 | + cn: null, | |
150 | + fileId: null, | |
151 | + bsrpTrvctList: [], | |
152 | + sanctnList: [], | |
153 | + }, | |
154 | + }; | |
155 | + }, | |
156 | + | |
157 | + computed: { | |
158 | + // 재신청 여부 | |
159 | + isReapplyMode() { | |
160 | + return this.pageMode === 'reapply'; | |
161 | + }, | |
162 | + | |
163 | + // 재신청 여부에 따른 등록 버튼 변경 | |
164 | + submitButtonText() { | |
165 | + return this.isReapplyMode ? '재신청' : '수정'; | |
166 | + } | |
167 | + }, | |
168 | + | |
169 | + async created() { | |
170 | + this.pageId = this.$route.query.id; | |
171 | + this.pageMode = this.$route.query.type; | |
172 | + if (this.$isEmpty(this.pageId)) { | |
173 | + alert("게시물이 존재하지 않습니다."); | |
174 | + this.handleNavigation('list'); | |
175 | + return; | |
176 | + } | |
177 | + | |
178 | + this.sanctnCodes = await this.$findChildCodes('sanctn_code'); | |
179 | + this.defaultSanctnCode = this.sanctnCodes[0]?.code; | |
180 | + | |
181 | + this.trvctTypeCodes = await this.$findChildCodes('BSRP_TRVCT'); | |
182 | + this.travelExpenseCodes = await this.$findChildCodes('TRAVEL_EXPENSE'); | |
183 | + | |
184 | + }, | |
185 | + | |
186 | + async mounted() { | |
187 | + await this.fetchData(); // 출장 정보 조회 | |
188 | + }, | |
189 | + | |
190 | + methods: { | |
191 | + // 출장 정보 조회 | |
192 | + async fetchData() { | |
193 | + try { | |
194 | + const response = await findBsrpProc(this.pageId); | |
195 | + const result = response.data.data; | |
196 | + | |
197 | + this.bsrpInfo = result.bsrpInfoDTO; | |
198 | + this.cards = result.cards; | |
199 | + this.vhcles = result.vhcles; | |
200 | + this.hasBsrpRport = result.hasBsrpRport; | |
201 | + | |
202 | + if (this.hasBsrpRport) { | |
203 | + this.fetchReportData(); // 출장 복명 정보 조회 | |
204 | + } | |
205 | + } catch (error) { | |
206 | + alert('데이터 조회에 실패했습니다.'); | |
207 | + this.handleNavigation('list'); | |
208 | + } | |
209 | + }, | |
210 | + | |
211 | + // 출장 복명 정보 조회 | |
212 | + async fetchReportData() { | |
213 | + try { | |
214 | + const response = await findBsrpRportProc(this.pageId); | |
215 | + const result = response.data.data; | |
216 | + | |
217 | + this.bsrpRport = result; | |
218 | + if (this.bsrpRport.bsrpTrvctList && this.bsrpRport.fileList) { | |
219 | + this.bsrpRport.bsrpTrvctList.forEach(trvct => { | |
220 | + const file = this.bsrpRport.fileList.find(f => f.ordr === trvct.ordr); | |
221 | + if (file) { | |
222 | + trvct.fileName = file.fileNm; | |
223 | + trvct.isExistingFile = true; | |
224 | + } else { | |
225 | + this.initializeExpenseFile(trvct); | |
226 | + } | |
227 | + }); | |
228 | + } | |
229 | + } catch (error) { | |
230 | + alert('데이터 조회에 실패했습니다.'); | |
231 | + this.handleNavigation('list'); | |
232 | + } | |
233 | + }, | |
234 | + | |
235 | + // 이전 승인자 조회 핸들러 | |
236 | + async handleLoadLastApprovers() { | |
237 | + try { | |
238 | + const response = await findLastBsrpRportProc(); | |
239 | + const result = response.data.data; | |
240 | + | |
241 | + if (result == null || result.length == 0) { | |
242 | + alert("출장 복명 기록이 존재하지 않아, 이전 승인자를 불러올 수 없습니다."); | |
243 | + return; | |
244 | + } | |
245 | + | |
246 | + this.bsrpRport.sanctnList = result; | |
247 | + } catch (error) { | |
248 | + const message = error.response?.data?.message || "이전 승인자를 불러오는데 실패했습니다."; | |
249 | + alert(message); | |
250 | + } | |
251 | + }, | |
252 | + | |
253 | + // 저장 핸들러 | |
254 | + async handleSave() { | |
255 | + if (!this.validateForm()) return; | |
256 | + | |
257 | + try { | |
258 | + const formData = this.buildFormData(); | |
259 | + | |
260 | + this.bsrpRport.bsrpTrvctList.forEach(item => { | |
261 | + if (item.file) { | |
262 | + formData.append('files', item.file); | |
263 | + } | |
264 | + }); | |
265 | + | |
266 | + await saveBsrpRportProc(formData); | |
267 | + alert('출장 복명서가 등록되었습니다.'); | |
268 | + this.handleNavigation('view', this.pageId); | |
269 | + } catch (error) { | |
270 | + alert(error.response?.data?.message || '저장에 실패했습니다.'); | |
271 | + } | |
272 | + }, | |
273 | + | |
274 | + // 수정 핸들러 | |
275 | + async handleUpdate() { | |
276 | + if (!this.validateForm()) return; | |
277 | + | |
278 | + try { | |
279 | + const formData = this.buildFormData(); | |
280 | + | |
281 | + const oldFiles = []; | |
282 | + const newFileItems = []; | |
283 | + | |
284 | + this.bsrpRport.bsrpTrvctList.forEach(item => { | |
285 | + if (item.isExistingFile && item.ordr > 0) { | |
286 | + oldFiles.push(String(item.ordr)); | |
287 | + } else if (item.file) { | |
288 | + newFileItems.push(item); | |
289 | + } | |
290 | + }); | |
291 | + | |
292 | + formData.append('oldFiles', new Blob([JSON.stringify(oldFiles)], { type: 'application/json' })); | |
293 | + | |
294 | + newFileItems.forEach(item => { | |
295 | + formData.append('files', item.file); | |
296 | + }); | |
297 | + | |
298 | + await updateBsrpRport(formData); | |
299 | + const message = this.isReapplyMode ? "재신청되었습니다." : "수정되었습니다."; | |
300 | + alert(message); | |
301 | + | |
302 | + this.handleNavigation('view', this.pageId); | |
303 | + } catch (error) { | |
304 | + alert(error.response?.data?.message || '수정에 실패했습니다.'); | |
305 | + } | |
306 | + }, | |
307 | + | |
308 | + // 승인자 추가 핸들러 | |
309 | + handleApproverAdd(user) { | |
310 | + this.bsrpRport.sanctnList.push({ | |
311 | + confmerId: user.userId, | |
312 | + confmerNm: user.userNm, | |
313 | + clsf: user.clsf, | |
314 | + clsfNm: user.clsfNm, | |
315 | + sanctnOrdr: this.bsrpRport.sanctnList.length + 1, | |
316 | + sanctnIem: 'bsrp_rport', | |
317 | + sanctnSe: this.defaultSanctnCode, | |
318 | + }); | |
319 | + this.isOpenSanctnModal = false; | |
320 | + }, | |
321 | + | |
322 | + // 승인자 삭제 핸들러 | |
323 | + handleApproverRemove(idx) { | |
324 | + this.bsrpRport.sanctnList.splice(idx, 1); | |
325 | + this.bsrpRport.sanctnList.forEach((item, index) => { | |
326 | + item.sanctnOrdr = index + 1; | |
327 | + }); | |
328 | + }, | |
329 | + | |
330 | + // 여비 계산 추가 핸들러 | |
331 | + handleExpenseAdd() { | |
332 | + this.bsrpRport.bsrpTrvctList.push({ | |
333 | + se: this.cards.length > 0 ? 'CORPORATE' : 'PERSONAL', | |
334 | + setleMbyId: this.cards.length > 0 ? this.cards[0].cardId : this.userInfo.userId, | |
335 | + ty: '', | |
336 | + amount: null, | |
337 | + ordr: 0, | |
338 | + file: null, | |
339 | + fileName: '', | |
340 | + isExistingFile: false | |
341 | + }); | |
342 | + }, | |
343 | + | |
344 | + // 여비 계산 삭제 핸들러 | |
345 | + handleExpenseRemove(idx) { | |
346 | + this.bsrpRport.bsrpTrvctList.splice(idx, 1); | |
347 | + }, | |
348 | + | |
349 | + // 파일 업로드 핸들러 | |
350 | + handleFileUpload(idx, event) { | |
351 | + const file = event.target.files[0]; | |
352 | + if (!file) return; | |
353 | + | |
354 | + const trvct = this.bsrpRport.bsrpTrvctList[idx]; | |
355 | + trvct.file = file; | |
356 | + trvct.fileName = file.name; | |
357 | + trvct.isExistingFile = false; | |
358 | + }, | |
359 | + | |
360 | + // 취소 핸들러 | |
361 | + handleCancel() { | |
362 | + if (confirm('작성 중인 내용이 삭제됩니다. 계속하시겠습니까?')) { | |
363 | + this.handleNavigation('view', this.pageId); | |
364 | + } | |
365 | + }, | |
366 | + | |
367 | + // 페이지 이동 핸들러 | |
368 | + handleNavigation(type, id) { | |
369 | + const routeMap = { | |
370 | + 'list': { name: 'BsrpListPage' }, | |
371 | + 'view': { name: 'BsrpViewPage', query: { id } }, | |
372 | + 'bsrpInsert': { name: 'BsrpInsertPage', query: this.$isEmpty(id) ? {} : { id } }, | |
373 | + 'bsrpReapply': { name: 'BsrpInsertPage', query: { id, type: 'reapply' } }, | |
374 | + 'bsrpRportInsert': { name: 'BsrpRportInsertPage', query: this.$isEmpty(id) ? {} : { id } }, | |
375 | + 'bsrpRportReapply': { name: 'BsrpRportInsertPage', query: { id, type: 'reapply' } }, | |
376 | + }; | |
377 | + | |
378 | + const route = routeMap[type]; | |
379 | + if (route) { | |
380 | + this.$router.push(route); | |
381 | + } else { | |
382 | + alert("올바르지 않은 경로입니다."); | |
383 | + this.$router.push(routeMap['list']); | |
384 | + } | |
385 | + }, | |
386 | + | |
387 | + // 파일 정보 초기화 유틸리티 | |
388 | + initializeExpenseFile(trvct) { | |
389 | + trvct.ordr = 0; | |
390 | + trvct.isExistingFile = false; | |
391 | + trvct.file = null; | |
392 | + trvct.fileName = ''; | |
393 | + }, | |
394 | + | |
395 | + // 입력값 전체 유효성 검사 유틸리티 | |
396 | + validateForm() { | |
397 | + if (!this.bsrpRport.cn?.trim()) { | |
398 | + alert('복명내용을 입력해주세요.'); | |
399 | + return false; | |
400 | + } | |
401 | + | |
402 | + if (this.bsrpRport.sanctnList.length === 0) { | |
403 | + alert('승인자를 선택해주세요.'); | |
404 | + return false; | |
405 | + } | |
406 | + | |
407 | + for (let [idx, item] of this.bsrpRport.bsrpTrvctList.entries()) { | |
408 | + const num = idx + 1; | |
409 | + if (!item.se) { | |
410 | + alert(`${num}번째 여비계산의 결제 방식을 선택해주세요.`); | |
411 | + return false; | |
412 | + } | |
413 | + if (!item.setleMbyId) { | |
414 | + alert(`${num}번째 여비계산의 결제 주체를 선택해주세요.`); | |
415 | + return false; | |
416 | + } | |
417 | + if (!item.ty) { | |
418 | + alert(`${num}번째 여비계산의 출장비 구분을 선택해주세요.`); | |
419 | + return false; | |
420 | + } | |
421 | + if (!item.amount || item.amount <= 0) { | |
422 | + alert(`${num}번째 여비계산의 금액을 입력해주세요.`); | |
423 | + return false; | |
424 | + } | |
425 | + if (!item.fileName) { | |
426 | + alert(`${num}번째 여비계산의 영수증을 첨부해주세요.`); | |
427 | + return false; | |
428 | + } | |
429 | + } | |
430 | + return true; | |
431 | + }, | |
432 | + | |
433 | + // 데이터 가공 유틸리티 | |
434 | + buildFormData() { | |
435 | + const formData = new FormData(); | |
436 | + formData.append('bsrpId', this.pageId); | |
437 | + | |
438 | + const dto = { | |
439 | + cn: this.bsrpRport.cn, | |
440 | + sanctnList: this.bsrpRport.sanctnList.map(s => ({ | |
441 | + confmerId: s.confmerId, | |
442 | + clsf: s.clsf, | |
443 | + sanctnOrdr: s.sanctnOrdr, | |
444 | + sanctnIem: s.sanctnIem, | |
445 | + sanctnSe: s.sanctnSe, | |
446 | + })), | |
447 | + bsrpTrvctList: this.bsrpRport.bsrpTrvctList.map(t => ({ | |
448 | + se: t.se, | |
449 | + setleMbyId: t.setleMbyId, | |
450 | + ty: t.ty, | |
451 | + amount: t.amount, | |
452 | + ordr: t.ordr || 0, | |
453 | + })) | |
454 | + }; | |
455 | + | |
456 | + if (this.bsrpRport.fileId) { | |
457 | + dto.fileId = this.bsrpRport.fileId; | |
458 | + } | |
459 | + | |
460 | + formData.append('bsrpRportDTO', new Blob([JSON.stringify(dto)], { type: 'application/json' })); | |
461 | + return formData; | |
462 | + }, | |
463 | + }, | |
464 | +}; | |
465 | +</script>(파일 끝에 줄바꿈 문자 없음) |
+++ client/views/pages/Manager/attendance/BsrpView.vue
... | ... | @@ -0,0 +1,322 @@ |
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="bsrpCnsul.sanctnList.length > 0" :sanctns="bsrpCnsul.sanctnList" /> | |
8 | + <div class="tbl-wrap row g-3 needs-validation detail"> | |
9 | + <table> | |
10 | + <tbody> | |
11 | + <tr> | |
12 | + <th>출장구분</th> | |
13 | + <td>{{ bsrpInfo.bsrpSeNm }}</td> | |
14 | + </tr> | |
15 | + <tr> | |
16 | + <th>이름</th> | |
17 | + <td>{{ userInfo.userNm }}</td> | |
18 | + </tr> | |
19 | + <tr> | |
20 | + <th>부서</th> | |
21 | + <td>{{ userInfo.deptNm }}</td> | |
22 | + </tr> | |
23 | + <tr> | |
24 | + <th>직급</th> | |
25 | + <td>{{ userInfo.clsfNm }}</td> | |
26 | + </tr> | |
27 | + <tr> | |
28 | + <th>출장지</th> | |
29 | + <td>{{ bsrpInfo.bsrpPlace }}</td> | |
30 | + </tr> | |
31 | + <tr> | |
32 | + <th>출장목적</th> | |
33 | + <td>{{ bsrpInfo.bsrpPurps }}</td> | |
34 | + </tr> | |
35 | + <tr> | |
36 | + <th>동행자</th> | |
37 | + <td> | |
38 | + <template v-if="bsrpInfo.bsrpNmprList.length > 0"> | |
39 | + <template v-for="(item, idx) of bsrpInfo.bsrpNmprList" :key="idx"> | |
40 | + <span v-if="idx != 0">, </span> | |
41 | + <span>{{ item.triperNm }}</span> | |
42 | + </template> | |
43 | + </template> | |
44 | + </td> | |
45 | + </tr> | |
46 | + <tr> | |
47 | + <th>품의내용</th> | |
48 | + <td> | |
49 | + <ViewerComponent :content="bsrpCnsul.cn" /> | |
50 | + </td> | |
51 | + </tr> | |
52 | + <tr> | |
53 | + <th>법인카드</th> | |
54 | + <td> | |
55 | + <template v-if="cards.length > 0"> | |
56 | + <template v-for="(item, idx) of cards" :key="idx"> | |
57 | + <span v-if="idx != 0">, </span> | |
58 | + <span>{{ item.cardNm }}</span> | |
59 | + </template> | |
60 | + </template> | |
61 | + </td> | |
62 | + </tr> | |
63 | + <tr> | |
64 | + <th>법인차량</th> | |
65 | + <td> | |
66 | + <template v-if="vhcles.length > 0"> | |
67 | + <template v-for="(item, idx) of vhcles" :key="idx"> | |
68 | + <span v-if="idx != 0">, </span> | |
69 | + <span>{{ item.vhcleNm }}</span> | |
70 | + </template> | |
71 | + </template> | |
72 | + </td> | |
73 | + </tr> | |
74 | + <tr> | |
75 | + <th>품의 신청일</th> | |
76 | + <td>{{ bsrpCnsul.rgsde }}</td> | |
77 | + </tr> | |
78 | + <tr v-if="sanctnStatus === 'rejected'"> | |
79 | + <th>품의 반려사유</th> | |
80 | + <td>{{ getRejectionReason(bsrpCnsul.sanctnList) }}</td> | |
81 | + </tr> | |
82 | + </tbody> | |
83 | + </table> | |
84 | + </div> | |
85 | + </div> | |
86 | + <div v-if="!hasBsrpRport" class="buttons"> | |
87 | + <template v-if="isConsultationApprover"> | |
88 | + <button type="button" class="btn sm primary" @click="handleApproval('A')">승인</button> | |
89 | + <button type="button" class="btn sm btn-red" @click="showConsultationPopup = true">반려</button> | |
90 | + </template> | |
91 | + <template v-else-if="isApplicant"> | |
92 | + <template v-if="sanctnStatus === 'waiting'"> | |
93 | + <button type="button" class="btn sm btn-red" @click="handleDelete">신청취소</button> | |
94 | + <button type="button" class="btn sm secondary" @click="handleNavigation('bsrpInsert', pageId)">수정</button> | |
95 | + </template> | |
96 | + <template v-if="sanctnStatus === 'rejected'"> | |
97 | + <button type="button" class="btn sm secondary" @click="handleNavigation('bsrpReapply', pageId)">재신청</button> | |
98 | + </template> | |
99 | + <template v-if="sanctnStatus === 'approved'"> | |
100 | + <button type="button" class="btn sm primary" @click="handleNavigation('bsrpRportInsert', pageId)">복명서 작성</button> | |
101 | + </template> | |
102 | + </template> | |
103 | + <button type="button" class="btn sm tertiary" @click="handleNavigation('list')">목록</button> | |
104 | + </div> | |
105 | + <!-- 출장 복명서 --> | |
106 | + <BsrpRportView v-if="hasBsrpRport" :pageId="pageId" :pageMode="pageMode" :cards="cards" :users="combinedUserList" /> | |
107 | + </div> | |
108 | + </div> | |
109 | + <ReturnPopup v-if="showConsultationPopup" @close="showConsultationPopup = false" @confirm="handleRejection" /> | |
110 | +</template> | |
111 | +<script> | |
112 | +import ReturnPopup from '../../../component/Popup/ReturnPopup.vue'; | |
113 | +import SanctnViewList from '../../../component/Sanctn/SanctnViewList.vue'; | |
114 | +import ViewerComponent from '../../../component/editor/ViewerComponent.vue'; | |
115 | +import BsrpRportView from '../../../component/bsrp/BsrpRportView.vue'; | |
116 | +// API | |
117 | +import { findBsrpProc, deleteBsrpProc } from '../../../../resources/api/bsrp'; | |
118 | +import { updateConfmAtProc } from '../../../../resources/api/sanctns'; | |
119 | + | |
120 | +export default { | |
121 | + name: 'BsrpView', | |
122 | + | |
123 | + components: { | |
124 | + BsrpRportView, | |
125 | + ReturnPopup, | |
126 | + SanctnViewList, | |
127 | + ViewerComponent, | |
128 | + }, | |
129 | + | |
130 | + data() { | |
131 | + return { | |
132 | + pageId: null, | |
133 | + pageMode: null, | |
134 | + showConsultationPopup: false, | |
135 | + bsrpInfo: { | |
136 | + applcntId: null, | |
137 | + bsrpSeNm: null, | |
138 | + bsrpPlace: null, | |
139 | + bsrpPurps: null, | |
140 | + bsrpNmprList: [], | |
141 | + }, | |
142 | + userInfo: {}, | |
143 | + cards: [], | |
144 | + vhcles: [], | |
145 | + bsrpCnsul: { | |
146 | + cn: null, | |
147 | + rgsde: null, | |
148 | + sanctnList: [], | |
149 | + }, | |
150 | + hasBsrpRport: false, | |
151 | + returnResn: null, | |
152 | + }; | |
153 | + }, | |
154 | + | |
155 | + computed: { | |
156 | + // 결재 상태 | |
157 | + sanctnStatus() { | |
158 | + const sanctnList = this.bsrpCnsul.sanctnList; | |
159 | + if (sanctnList.length === 0) return 'none'; | |
160 | + | |
161 | + // 하나라도 반려된 경우 > 반려 | |
162 | + if (sanctnList.some(item => item.confmAt === 'R')) { | |
163 | + return 'rejected'; | |
164 | + } | |
165 | + | |
166 | + // 전부 승인된 경우 > 승인 | |
167 | + if (sanctnList.every(item => item.confmAt === 'A')) { | |
168 | + return 'approved'; | |
169 | + } | |
170 | + | |
171 | + // 그 외 > 대기 | |
172 | + return 'waiting'; | |
173 | + }, | |
174 | + | |
175 | + // 작성자 여부 | |
176 | + isApplicant() { | |
177 | + return this.bsrpInfo.applcntId === this.$store.state.userInfo.userId; | |
178 | + }, | |
179 | + | |
180 | + // 결재자 여부 | |
181 | + isConsultationApprover() { | |
182 | + const sanctnList = this.bsrpCnsul.sanctnList; | |
183 | + const mySanctn = sanctnList.find( | |
184 | + item => item.confmerId == this.$store.state.userInfo.userId | |
185 | + ); | |
186 | + return mySanctn && mySanctn.confmAt === 'W' && this.pageMode === 'sanctns'; | |
187 | + }, | |
188 | + | |
189 | + // 출장 신청자 + 동행자 목록 | |
190 | + combinedUserList() { | |
191 | + const userList = []; | |
192 | + | |
193 | + // 신청자 추가 | |
194 | + userList.push({ | |
195 | + userId: this.userInfo.userId, | |
196 | + userNm: this.userInfo.userNm, | |
197 | + }); | |
198 | + | |
199 | + // 동행자 추가 | |
200 | + if (this.bsrpInfo.bsrpNmprList && this.bsrpInfo.bsrpNmprList.length > 0) { | |
201 | + this.bsrpInfo.bsrpNmprList.forEach(nmpr => { | |
202 | + userList.push({ | |
203 | + userId: nmpr.triperId, | |
204 | + userNm: nmpr.triperNm | |
205 | + }); | |
206 | + }); | |
207 | + } | |
208 | + | |
209 | + return userList; | |
210 | + } | |
211 | + }, | |
212 | + | |
213 | + async created() { | |
214 | + this.pageId = this.$route.query.id; | |
215 | + this.pageMode = this.$route.query.type; | |
216 | + if (this.$isEmpty(this.pageId)) { | |
217 | + alert("게시물이 존재하지 않습니다."); | |
218 | + this.handleNavigation('list'); | |
219 | + } | |
220 | + }, | |
221 | + | |
222 | + mounted() { | |
223 | + this.fetchData(); // 출장 정보 조회 | |
224 | + }, | |
225 | + | |
226 | + methods: { | |
227 | + // 출장 정보 조회 | |
228 | + async fetchData() { | |
229 | + try { | |
230 | + const response = await findBsrpProc(this.pageId); | |
231 | + const result = response.data.data; | |
232 | + | |
233 | + this.bsrpInfo = result.bsrpInfoDTO; | |
234 | + this.userInfo = result.userViewDTO; | |
235 | + this.cards = result.cards; | |
236 | + this.vhcles = result.vhcles; | |
237 | + this.bsrpCnsul = result.bsrpCnsulDTO; | |
238 | + this.hasBsrpRport = result.hasBsrpRport; | |
239 | + } catch (error) { | |
240 | + const message = error.response?.data?.message || '데이터 조회에 실패했습니다.'; | |
241 | + alert(message); | |
242 | + } | |
243 | + }, | |
244 | + | |
245 | + // 결재 | |
246 | + async handleApproval(value) { | |
247 | + try { | |
248 | + const sanctnList = this.bsrpCnsul.sanctnList; | |
249 | + const sanctn = sanctnList.find(item => item.confmerId == this.$store.state.userInfo.userId); | |
250 | + | |
251 | + if (!sanctn) { | |
252 | + alert('결재 권한이 없습니다.'); | |
253 | + return; | |
254 | + } | |
255 | + | |
256 | + const data = { | |
257 | + sanctnOrdr: sanctn.sanctnOrdr, | |
258 | + sanctnIem: sanctn.sanctnIem, | |
259 | + sanctnMbyId: this.pageId, | |
260 | + confmAt: value, | |
261 | + returnResn: this.returnResn, | |
262 | + }; | |
263 | + | |
264 | + await updateConfmAtProc(sanctn.sanctnId, data); | |
265 | + alert("승인했습니다."); | |
266 | + this.fetchData(); | |
267 | + } catch (error) { | |
268 | + const message = error.response?.data?.message || '승인 처리에 실패했습니다.'; | |
269 | + alert(message); | |
270 | + } | |
271 | + }, | |
272 | + | |
273 | + // 출장 취소 (완전 삭제) | |
274 | + async handleDelete() { | |
275 | + const isCheck = confirm("출장 신청을 취소하시겠습니까?"); | |
276 | + if (!isCheck) return; | |
277 | + | |
278 | + try { | |
279 | + await deleteBsrpProc(this.pageId); | |
280 | + alert("출장 신청이 취소되었습니다."); | |
281 | + this.handleNavigation('list'); | |
282 | + } catch (error) { | |
283 | + const message = error.response?.data?.message || '삭제에 실패했습니다.'; | |
284 | + alert(message); | |
285 | + } | |
286 | + }, | |
287 | + | |
288 | + // 반려 결재 핸들러 | |
289 | + async handleRejection(reason) { | |
290 | + this.returnResn = reason; | |
291 | + await this.handleApproval('R'); | |
292 | + this.showConsultationPopup = false; | |
293 | + }, | |
294 | + | |
295 | + // 페이지 이동 핸들러 | |
296 | + handleNavigation(type, id) { | |
297 | + const routeMap = { | |
298 | + 'list': { name: this.pageMode === 'sanctns' ? 'PendingApprovalListPage' : 'BsrpListPage' }, | |
299 | + 'view': { name: 'BsrpViewPage', query: { id } }, | |
300 | + 'bsrpInsert': { name: 'BsrpInsertPage', query: this.$isEmpty(id) ? {} : { id } }, | |
301 | + 'bsrpReapply': { name: 'BsrpInsertPage', query: { id, type: 'reapply' } }, | |
302 | + 'bsrpRportInsert': { name: 'BsrpRportInsertPage', query: this.$isEmpty(id) ? {} : { id } }, | |
303 | + 'bsrpRportReapply': { name: 'BsrpRportInsertPage', query: { id, type: 'reapply' } }, | |
304 | + }; | |
305 | + | |
306 | + const route = routeMap[type]; | |
307 | + if (route) { | |
308 | + this.$router.push(route); | |
309 | + } else { | |
310 | + alert("올바르지 않은 경로입니다."); | |
311 | + this.$router.push(routeMap['list']); | |
312 | + } | |
313 | + }, | |
314 | + | |
315 | + // 반려 사유 유틸리티 | |
316 | + getRejectionReason(sanctnList) { | |
317 | + const rejectedItem = sanctnList.find(item => item.confmAt === 'R'); | |
318 | + return rejectedItem?.returnResn || '-'; | |
319 | + }, | |
320 | + }, | |
321 | +}; | |
322 | +</script>(파일 끝에 줄바꿈 문자 없음) |
--- client/views/pages/Manager/attendance/ChuljangBokmyeongDetail.vue
... | ... | @@ -1,142 +0,0 @@ |
1 | -<template> | |
2 | -<div class="card "> | |
3 | - <div class="card-body"> | |
4 | - <h2 class="card-title">출장 현황</h2> | |
5 | - | |
6 | - <div class="form-card"> | |
7 | - <h1>출장복명서</h1> | |
8 | - <div class="approval-box tbl-wrap tbl2"> | |
9 | - <table class="tbl data"> | |
10 | - <tbody> | |
11 | - <tr class="thead"> | |
12 | - <td rowspan="2" class="th">승인자</td> | |
13 | - <td v-for="(approver, index) in approvers" :key="index"> | |
14 | - <p class="position">{{ approver.position }}</p> | |
15 | - </td> | |
16 | - </tr> | |
17 | - <tr> | |
18 | - <td v-for="(approver, index) in approvers" :key="index"> | |
19 | - <p class="name">{{ approver.name }}</p> | |
20 | - <p class="date">{{ approver.date }}</p> | |
21 | - </td> | |
22 | - </tr> | |
23 | - </tbody> | |
24 | - | |
25 | - </table> | |
26 | - </div> | |
27 | - <form class="row g-3 needs-validation detail" :class="{ 'was-validated': formSubmitted }" @submit.prevent="handleRegister" novalidate> | |
28 | - | |
29 | - <div class="col-12 chuljang"> | |
30 | - <label for="yourName" class="form-label">복명내용</label> | |
31 | - <input v-model="name" type="text" name="name" class="form-control textarea " readonly> | |
32 | - </div> | |
33 | - <div class="col-12"> | |
34 | - <label for="yourName" class="form-label">여비계산</label> | |
35 | - <input v-model="name" type="text" name="name" class="form-control " readonly> | |
36 | - </div> | |
37 | - <div class="col-12 "> | |
38 | - <label for="yourName" class="form-label">복명 신청일</label> | |
39 | - <input v-model="name" type="text" name="name" class="form-control" readonly placeholder="2025-01-01"> | |
40 | - </div> | |
41 | - <div class="col-12 border-x return"> | |
42 | - <label for="yourName" class="form-label">반려사유</label> | |
43 | - <input v-model="name" type="text" name="name" class="form-control" readonly placeholder="2025-01-01"> | |
44 | - </div> | |
45 | - | |
46 | - | |
47 | - </form> | |
48 | - </div> | |
49 | - <div class="buttons"> | |
50 | - <button class="btn sm primary" type="submit">승인</button> | |
51 | - <button class="btn sm btn-red" type="submit" @click="showPopup = true">반려</button> | |
52 | - <button class="btn sm btn-red" type="submit">신청취소</button> | |
53 | - <button class="btn sm secondary" type="submit">재신청</button> | |
54 | - <button class="btn sm secondary" type="submit">수정</button> | |
55 | - <button class="btn sm tertiary " type="submit">목록</button> | |
56 | - </div> | |
57 | - <ReturnPopup v-if="showPopup" @close="showPopup = false"/> | |
58 | - | |
59 | - </div> | |
60 | - </div> | |
61 | -</template> | |
62 | - | |
63 | -<script> | |
64 | -import ReturnPopup from '../../../component/Popup/ReturnPopup.vue'; | |
65 | -export default { | |
66 | - data() { | |
67 | - const today = new Date().toISOString().split('T')[0]; | |
68 | - return { | |
69 | - showPopup: false, | |
70 | - startDate: today, | |
71 | - startTime: "09:00", // 기본 시작 시간 09:00 | |
72 | - endDate: today, | |
73 | - endTime: "18:00", // 기본 종료 시간 18:00 | |
74 | - category: "", | |
75 | - dayCount: 1, | |
76 | - reason: "", // 사유 | |
77 | - approvers: [ | |
78 | - { position: '', name: '', date: '' }, | |
79 | - { position: '', name: '', date: '' }, | |
80 | - ], | |
81 | - listData: [ | |
82 | - { | |
83 | - type: '연차', | |
84 | - approvalType: '결재', | |
85 | - applicant: '홍길동', | |
86 | - period: '2025-05-10 ~ 2025-15-03', | |
87 | - requestDate: '2025-04-25', | |
88 | - status: '대기' | |
89 | - }, { | |
90 | - type: '반차', | |
91 | - approvalType: '전결', | |
92 | - applicant: '홍길동', | |
93 | - period: '2025-05-01 ~ 2025-05-03', | |
94 | - requestDate: '2025-04-25', | |
95 | - status: '승인' | |
96 | - }], | |
97 | - }; | |
98 | - }, | |
99 | - components: { | |
100 | - ReturnPopup | |
101 | - }, | |
102 | - computed: { | |
103 | - }, | |
104 | - methods: { | |
105 | - hasAnyApprover() { | |
106 | - return this.approvers.some( | |
107 | - (approver) => | |
108 | - approver.name?.trim() !== '' && approver.date?.trim() !== '' | |
109 | - ); | |
110 | - }, | |
111 | - calculateDayCount() { | |
112 | - const start = new Date(`${this.startDate}T${this.startTime}:00`); | |
113 | - const end = new Date(`${this.endDate}T${this.endTime}:00`); | |
114 | - | |
115 | - let totalHours = (end - start) / (1000 * 60 * 60); // 밀리초를 시간 단위로 변환 | |
116 | - | |
117 | - if (this.startDate !== this.endDate) { | |
118 | - // 시작일과 종료일이 다른경우 | |
119 | - const startDateObj = new Date(this.startDate); | |
120 | - const endDateObj = new Date(this.endDate); | |
121 | - const daysDifference = (endDateObj - startDateObj) / (1000 * 60 * 60 * 24); // 두 날짜 사이의 차이를 일수로 계산 | |
122 | - if (this.startTime !== "09:00" || this.endTime !== "18:00") { | |
123 | - this.dayCount = daysDifference + 0.5; // 시간 조건이 기준에서 벗어날 경우 | |
124 | - } else { | |
125 | - this.dayCount = Math.ceil(daysDifference + 1); // 시간 조건이 기준에 맞을 경우 | |
126 | - } | |
127 | - } else { | |
128 | - // 시작일과 종료일이 같은 경우 | |
129 | - if (this.startTime !== "09:00" || this.endTime !== "18:00") { | |
130 | - this.dayCount = 0.5; // 시작 시간 또는 종료 시간이 기준과 다를 경우 0.5 | |
131 | - } else { | |
132 | - this.dayCount = 1; // 기준 시간(09:00~18:00)이 맞으면 1일로 간주 | |
133 | - } | |
134 | - } | |
135 | - | |
136 | - | |
137 | - }, | |
138 | - | |
139 | - | |
140 | - }, | |
141 | -}; | |
142 | -</script> |
--- client/views/pages/Manager/attendance/ChuljangDetailAll.vue
... | ... | @@ -1,262 +0,0 @@ |
1 | -<template> | |
2 | - <div class="card"> | |
3 | - <div class="card-body"> | |
4 | - <h2 class="card-title">출장 현황</h2> | |
5 | - <div class="form-card" style="margin-bottom: 20px;"> | |
6 | - <h1>출장품의서</h1> | |
7 | - <SanctnViewList v-if="bsrpCnsul.sanctnList.length > 0" :sanctns="bsrpCnsul.sanctnList" /> | |
8 | - <form class="row g-3 needs-validation detail" :class="{ 'was-validated': formSubmitted }" @submit.prevent="handleRegister" novalidate> | |
9 | - <div class="col-12"> | |
10 | - <div class="col-12 border-x"> | |
11 | - <label class="form-label">출장구분</label> | |
12 | - <p>{{ bsrpInfo.bsrpSeNm }}</p> | |
13 | - </div> | |
14 | - <div class="col-12 border-x"> | |
15 | - <label class="form-label">이름</label> | |
16 | - <p>{{ user.userNm }}</p> | |
17 | - </div> | |
18 | - </div> | |
19 | - <div class="col-12"> | |
20 | - <div class="col-12 border-x"> | |
21 | - <label class="form-label">부서</label> | |
22 | - <p>{{ user.deptNm }}</p> | |
23 | - </div> | |
24 | - <div class="col-12 border-x"> | |
25 | - <label class="form-label">직급</label> | |
26 | - <p>{{ user.clsfNm }}</p> | |
27 | - </div> | |
28 | - </div> | |
29 | - <div class="col-12"> | |
30 | - <label class="form-label">출장지</label> | |
31 | - <p>{{ bsrpInfo.bsrpPlace }}</p> | |
32 | - </div> | |
33 | - <div class="col-12"> | |
34 | - <label class="form-label">출장목적</label> | |
35 | - <p>{{ bsrpInfo.bsrpPurps }}</p> | |
36 | - </div> | |
37 | - <div class="col-12"> | |
38 | - <label class="form-label">동행자</label> | |
39 | - <div v-if="bsrpInfo.bsrpNmprList.length > 0"> | |
40 | - <span v-for="(item, idx) of bsrpInfo.bsrpNmprList" :key="idx">{{ item.triperNm }}</span> | |
41 | - </div> | |
42 | - </div> | |
43 | - <div class="col-12"> | |
44 | - <label class="form-label">품의내용</label> | |
45 | - <ViewerComponent :content="bsrpCnsul.cn" /> | |
46 | - </div> | |
47 | - <div class="col-12"> | |
48 | - <label class="form-label">법인카드</label> | |
49 | - <div> | |
50 | - <p v-for="(item, idx) of cprCardList" :key="idx"> | |
51 | - <span v-if="idx !== 0">, </span> | |
52 | - <span>{{ item.cardNm }}</span> | |
53 | - </p> | |
54 | - </div> | |
55 | - </div> | |
56 | - <div class="col-12"> | |
57 | - <label class="form-label">법인차량</label> | |
58 | - <div> | |
59 | - <p v-for="(item, idx) of cprVhcleList" :key="idx"> | |
60 | - <span v-if="idx !== 0">, </span> | |
61 | - <span>{{ item.vhcleNm }}</span> | |
62 | - </p> | |
63 | - </div> | |
64 | - </div> | |
65 | - <div class="col-12 border-x"> | |
66 | - <label class="form-label">품의 신청일</label> | |
67 | - <p>{{ bsrpCnsul.rgsde }}</p> | |
68 | - </div> | |
69 | - </form> | |
70 | - </div> | |
71 | - <div v-if="!$isEmpty(bsrpRport.bsrpId)" class="form-card"> | |
72 | - <h1>출장복명서</h1> | |
73 | - <SanctnViewList v-if="bsrpRport.sanctnList.length > 0" :sanctns="bsrpRport.sanctnList" /> | |
74 | - <form class="row g-3 needs-validation detail" :class="{ 'was-validated': formSubmitted }" @submit.prevent="handleRegister" novalidate> | |
75 | - <div class="col-12"> | |
76 | - <label class="form-label">복명내용</label> | |
77 | - <ViewerComponent :content="bsrpRport.cn" /> | |
78 | - </div> | |
79 | - <div class="col-12"> | |
80 | - <label class="form-label">여비계산</label> | |
81 | - <p>{{ bsrpInfo.applcntId }}</p> | |
82 | - </div> | |
83 | - <div class="col-12"> | |
84 | - <label class="form-label">복명 신청일</label> | |
85 | - <p>{{ bsrpRport.rgsde }}</p> | |
86 | - </div> | |
87 | - <div class="col-12 border-x return"> | |
88 | - <label class="form-label">반려사유</label> | |
89 | - <p>{{ bsrpInfo.applcntId }}</p> | |
90 | - </div> | |
91 | - </form> | |
92 | - </div> | |
93 | - <div class="buttons"> | |
94 | - <button v-if="sanctnStatus === 'waiting' && sanctnStatus !== 'approved'" type="button" class="btn sm btn-red" @click="deleteData">신청취소</button> | |
95 | - <button v-if="sanctnStatus === 'waiting'" type="button" class="btn sm secondary" @click="fnMoveTo('edit', pageId)">수정</button> | |
96 | - <button v-if="sanctnStatus === 'rejected'" type="button" class="btn sm secondary" @click="fnMoveTo('edit', pageId)">재신청</button> | |
97 | - <button v-if="sanctnStatus === 'approved'" type="button" class="btn sm primary" @click="fnMoveTo('plus', pageId)">복명서 작성</button> | |
98 | - <button type="button" class="btn sm tertiary" @click="fnMoveTo('list')">목록</button> | |
99 | - </div> | |
100 | - </div> | |
101 | - </div> | |
102 | -</template> | |
103 | -<script> | |
104 | -import SanctnViewList from '../../../component/Sanctn/SanctnViewList.vue'; | |
105 | -import ViewerComponent from '../../../component/editor/ViewerComponent.vue'; | |
106 | -// API | |
107 | -import { bsrpProc, deleteBsrpProc } from '../../../../resources/api/bsrp'; | |
108 | - | |
109 | -export default { | |
110 | - components: { | |
111 | - SanctnViewList, ViewerComponent, | |
112 | - }, | |
113 | - | |
114 | - data() { | |
115 | - return { | |
116 | - pageId: null, | |
117 | - | |
118 | - // 출장 정보 | |
119 | - bsrpInfo: { | |
120 | - bsrpId: null, // 출장 아이디 | |
121 | - applcntId: null, // 신청자 아이디 | |
122 | - bsrpSe: null, // 출장구분 (공통코드 : sanctn_mby_bsrp) | |
123 | - bsrpPlace: null, // 출장지 | |
124 | - bsrpPurps: null, // 출장목적 | |
125 | - bgnde: null, // 시작일 | |
126 | - beginHour: null, // 시작시 | |
127 | - beginMnt: null, // 시작분 | |
128 | - endde: null, // 종료일 | |
129 | - endHour: null, // 종료시 | |
130 | - endMnt: null, // 종료분 | |
131 | - bsrpSeNm: null, // 출장구분 이름 | |
132 | - bsrpNmprList: [], // 출장 인원 | |
133 | - }, | |
134 | - // 품의서 | |
135 | - bsrpCnsul: { | |
136 | - bsrpId: null, // 출장 아이디 | |
137 | - cn: null, // 내용 | |
138 | - confmAt: null, // 승인 여부 (W: 대기, A: 승인, R 반려) | |
139 | - rgsde: null, // 등록일 | |
140 | - register: null, // 등록자 | |
141 | - updde: null, // 수정일 | |
142 | - updusr: null, // 수정자 | |
143 | - sanctnList: [], | |
144 | - }, | |
145 | - // 복명서 | |
146 | - bsrpRport: { | |
147 | - bsrpId: null, // 출장 아이디 | |
148 | - cn: null, // 내용 | |
149 | - confmAt: null, // 승인 여부 (W: 대기, A: 승인, R 반려) | |
150 | - fileId: null, // 파일 아이디 | |
151 | - rgsde: null, // 등록일 | |
152 | - register: null, // 등록자 | |
153 | - updde: null, // 수정일 | |
154 | - updusr: null, // 수정자 | |
155 | - sanctnList: [], | |
156 | - }, | |
157 | - // 신청자 정보 | |
158 | - user: {}, | |
159 | - }; | |
160 | - }, | |
161 | - | |
162 | - computed: { | |
163 | - // 결재 상태 확인 | |
164 | - sanctnStatus() { | |
165 | - const sanctnList = this.bsrpCnsul.sanctnList; | |
166 | - | |
167 | - if (sanctnList.length === 0) return 'none'; // 결재 정보 없음 | |
168 | - | |
169 | - // 하나라도 반려가 있으면 '반려' | |
170 | - if (sanctnList.some(item => item.sanctnSttus === 'R')) { | |
171 | - return 'rejected'; | |
172 | - } | |
173 | - | |
174 | - // 모든 결재가 승인이면 '승인' | |
175 | - if (sanctnList.every(item => item.sanctnSttus === 'A')) { | |
176 | - return 'approved'; | |
177 | - } | |
178 | - | |
179 | - // 그 외에는 '대기' | |
180 | - return 'waiting'; | |
181 | - } | |
182 | - }, | |
183 | - | |
184 | - async created() { | |
185 | - this.pageId = this.$route.query.id; | |
186 | - if (this.$isEmpty(this.pageId)) { | |
187 | - alert("게시물이 존재하지 않습니다."); | |
188 | - this.fnMoveTo('list'); | |
189 | - } | |
190 | - }, | |
191 | - | |
192 | - mounted() { | |
193 | - this.findDatas(); // 상세 조회 | |
194 | - }, | |
195 | - | |
196 | - methods: { | |
197 | - // 상세 조회 | |
198 | - async findDatas() { | |
199 | - try { | |
200 | - const response = await bsrpProc(this.pageId); | |
201 | - const result = response.data.data; | |
202 | - | |
203 | - this.bsrpInfo = result.bsrpInfo; | |
204 | - this.bsrpInfo.bgnde = this.bsrpInfo.bgnde.split(' ')[0]; | |
205 | - this.bsrpInfo.endde = this.bsrpInfo.endde.split(' ')[0]; | |
206 | - | |
207 | - this.cprCardList = result.cprCardList; | |
208 | - this.cprVhcleList = result.cprVhcleList; | |
209 | - this.user = result.user; | |
210 | - | |
211 | - this.bsrpCnsul = result.bsrpCnsul; | |
212 | - this.bsrpRport = result.bsrpRport; | |
213 | - } catch (error) { | |
214 | - const message = error.response.data.message; | |
215 | - alert(message); | |
216 | - } | |
217 | - }, | |
218 | - | |
219 | - // 삭제 | |
220 | - async deleteData() { | |
221 | - const isCheck = confirm("삭제하시겠습니까?"); | |
222 | - if (!isCheck) { | |
223 | - return; | |
224 | - } | |
225 | - | |
226 | - try { | |
227 | - const response = await deleteBsrpProc(this.pageId); | |
228 | - | |
229 | - this.fnMoveTo('list'); | |
230 | - } catch (error) { | |
231 | - if (error.response) { | |
232 | - alert(error.response.data.message); | |
233 | - } else { | |
234 | - alert("에러가 발생했습니다."); | |
235 | - } | |
236 | - console.error(error.message); | |
237 | - this.fnMoveTo('list'); | |
238 | - } | |
239 | - }, | |
240 | - | |
241 | - // 페이지 이동 | |
242 | - fnMoveTo(type, id) { | |
243 | - const routes = { | |
244 | - 'list': { name: 'ChuljangStatue' }, | |
245 | - 'view': { name: 'ChuljangDetailAll', query: { id } }, | |
246 | - 'edit': { name: 'ChuljangInsert', query: this.$isEmpty(id) ? {} : { id } }, | |
247 | - 'plus': { name: 'ChuljangInsert', query: this.$isEmpty(id) ? {} : { id } }, | |
248 | - }; | |
249 | - | |
250 | - if (routes[type]) { | |
251 | - if (!this.$isEmpty(this.pageId) && type === 'list') { | |
252 | - this.$router.push({ name: 'ChuljangDetailAll', query: { id: this.pageId } }); | |
253 | - } | |
254 | - this.$router.push(routes[type]); | |
255 | - } else { | |
256 | - alert("올바르지 않은 경로를 요청하여 목록으로 이동합니다."); | |
257 | - this.$router.push(routes['list']); | |
258 | - } | |
259 | - }, | |
260 | - }, | |
261 | -}; | |
262 | -</script> |
--- client/views/pages/Manager/attendance/ChuljangInsert.vue
... | ... | @@ -1,432 +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>출장구분 <span class="require"><img :src="require" alt=""></span></th> | |
11 | - <td> | |
12 | - <select class="form-select sm" style="width: 110px;" v-model="bsrpInfo.bsrpSe"> | |
13 | - <option v-for="(item, idx) of cmmnCodes.bsrpCodeList" :key="idx" :value="item.code"> | |
14 | - {{ item.codeNm }} | |
15 | - </option> | |
16 | - </select> | |
17 | - </td> | |
18 | - <th>일수</th> | |
19 | - <td> | |
20 | - <input type="text" class="form-control sm" v-model="totalDays" readonly /> | |
21 | - </td> | |
22 | - </tr> | |
23 | - | |
24 | - <tr> | |
25 | - <th>출장지 <span class="require"><img :src="require" alt=""></span></th> | |
26 | - <td> | |
27 | - <input type="text" class="form-control sm" v-model="bsrpInfo.bsrpPlace" /> | |
28 | - </td> | |
29 | - <th>출장목적 <span class="require"><img :src="require" alt=""></span></th> | |
30 | - <td> | |
31 | - <input type="text" class="form-control sm" v-model="bsrpInfo.bsrpPurps" /> | |
32 | - </td> | |
33 | - </tr> | |
34 | - | |
35 | - <tr> | |
36 | - <th>출장기간 <span class="require"><img :src="require" alt=""></span></th> | |
37 | - <td colspan="3"> | |
38 | - <div class="d-flex"> | |
39 | - <div class="d-flex gap-1 mb-1"> | |
40 | - <input type="date" class="form-control sm" v-model="bsrpInfo.bgnde" /> | |
41 | - <input type="text" class="form-control sm" style="width: 100px;" placeholder="시" v-model="bsrpInfo.beginHour" /> | |
42 | - <input type="text" class="form-control sm" style="width: 100px;" placeholder="분" v-model="bsrpInfo.beginMnt" /> | |
43 | - </div> | |
44 | - <div class="d-flex gap-1"> | |
45 | - <input type="date" class="form-control sm" v-model="bsrpInfo.endde" /> | |
46 | - <input type="text" class="form-control sm" style="width: 100px;" placeholder="시" v-model="bsrpInfo.endHour" /> | |
47 | - <input type="text" class="form-control sm" style="width: 100px;" placeholder="분" v-model="bsrpInfo.endMnt" /> | |
48 | - </div> | |
49 | - </div> | |
50 | - </td> | |
51 | - </tr> | |
52 | - | |
53 | - | |
54 | - <tr> | |
55 | - <th> | |
56 | - 동행자 | |
57 | - </th> | |
58 | - <td> | |
59 | - <button type="button" title="추가" @click="isOpenNmprModal = true"> | |
60 | - <PlusCircleFilled /> | |
61 | - </button> | |
62 | - <HrPopup v-if="isOpenNmprModal" :lists="bsrpInfo.bsrpNmprList" @onSelected="fnAddNmpr" @close="isOpenNmprModal = false" /> | |
63 | - <div class="approval-container"> | |
64 | - <div v-for="(item, idx) of bsrpInfo.bsrpNmprList" :key="idx" class="d-flex addapproval"> | |
65 | - <div class="d-flex align-items-center border-x"> | |
66 | - <p>{{ item.triperNm }} {{ item.clsfNm }}</p> | |
67 | - <button type="button" @click="fnDelNmpr(idx)" @mousedown.stop> | |
68 | - <CloseOutlined /> | |
69 | - </button> | |
70 | - </div> | |
71 | - </div> | |
72 | - </div> | |
73 | - </td> | |
74 | - <th> | |
75 | - 승인자 <span class="require"><img :src="require" alt=""></span> | |
76 | - </th> | |
77 | - <td> | |
78 | - <button type="button" title="추가" @click="isOpenSanctnModal = true"> | |
79 | - <PlusCircleFilled /> | |
80 | - </button> | |
81 | - <HrPopup v-if="isOpenSanctnModal" :lists="bsrpCnsul.sanctnList" @onSelected="fnAddSanctn" @close="isOpenSanctnModal = false" /> | |
82 | - <div class="approval-container"> | |
83 | - <SanctnList v-model:lists="bsrpCnsul.sanctnList" @delSanctn="fnDelSanctn" /> | |
84 | - </div> | |
85 | - </td> | |
86 | - </tr> | |
87 | - <tr> | |
88 | - <th>품의내용 <span class="require"><img :src="require" alt=""></span></th> | |
89 | - <td colspan="3" style="height: calc(100% - 550px);"> | |
90 | - <EditorComponent v-model:contents="bsrpCnsul.cn" /> | |
91 | - </td> | |
92 | - </tr> | |
93 | - <tr> | |
94 | - <th> | |
95 | - 법인카드 | |
96 | - <button type="button" title="추가" @click="isOpenCardModal = true"> | |
97 | - <PlusCircleFilled /> | |
98 | - </button> | |
99 | - </th> | |
100 | - <td> | |
101 | - <CorpCardPopup v-if="isOpenCardModal" :bsrpInfo="bsrpInfo" :lists="cprCardList" @close="isOpenCardModal = false" @onSelected="fnAddCard" /> | |
102 | - <div class="approval-container"> | |
103 | - <div v-for="(card, idx) in cprCardList" :key="idx" class="d-flex gap-2 addapproval mb-2"> | |
104 | - <form class="d-flex align-items-center border-x"> | |
105 | - <p>{{ card.cardNm }}</p> | |
106 | - <button type="button" @click="fnDelCard(idx)" class="delete-button"> | |
107 | - <CloseOutlined /> | |
108 | - </button> | |
109 | - </form> | |
110 | - </div> | |
111 | - </div> | |
112 | - </td> | |
113 | - <th> | |
114 | - 법인차량 | |
115 | - <button type="button" title="추가" @click="isOpenVhcleModal = true"> | |
116 | - <PlusCircleFilled /> | |
117 | - </button> | |
118 | - </th> | |
119 | - <td> | |
120 | - <CorpCarPopup v-if="isOpenVhcleModal" :bsrpInfo="bsrpInfo" :lists="cprVhcleList" @close="isOpenVhcleModal = false" @onSelected="fnAddVhcle" /> | |
121 | - <div class="approval-container"> | |
122 | - <div v-for="(vhcle, idx) in cprVhcleList" :key="idx" class="d-flex gap-2 addapproval mb-2"> | |
123 | - <p>{{ vhcle.vhcleNm }}</p> | |
124 | - <select class="form-select" v-model="vhcle.drverId"> | |
125 | - <option value="" disabled hidden>운전자 선택</option> | |
126 | - <option :value="userInfo.userId">{{ userInfo.userNm }}</option> | |
127 | - <option v-for="(item, idx) of bsrpInfo.bsrpNmprList" :key="idx" :value="item.userId"> | |
128 | - {{ item.userNm }} | |
129 | - </option> | |
130 | - </select> | |
131 | - <button type="button" @click="fnDelVhcle(idx)" class="delete-button"> | |
132 | - <CloseOutlined /> | |
133 | - </button> | |
134 | - </div> | |
135 | - </div> | |
136 | - </td> | |
137 | - </tr> | |
138 | - </tbody> | |
139 | - </table> | |
140 | - </div> | |
141 | - | |
142 | - <div class="buttons"> | |
143 | - <button type="button" class="btn sm sm primary" @click="fnSave">신청</button> | |
144 | - <button v-if="$isEmpty(pageId)" type="button" class="btn sm sm secondary" @click="fnMoveTo('list')">목록</button> | |
145 | - <button v-else type="button" class="btn sm sm secondary" @click="fnMoveTo('view', pageId)">취소</button> | |
146 | - </div> | |
147 | - </div> | |
148 | - </div> | |
149 | -</template> | |
150 | -<script> | |
151 | -import { PlusCircleFilled, CloseOutlined } from '@ant-design/icons-vue'; | |
152 | -import HrPopup from '../../../component/Popup/HrPopup.vue'; | |
153 | -import CorpCarPopup from '../../../component/Popup/CorpCarPopup.vue'; | |
154 | -import CorpCardPopup from '../../../component/Popup/CorpCardPopup.vue'; | |
155 | -import SanctnList from '../../../component/Sanctn/SanctnFormList.vue'; | |
156 | -import EditorComponent from '../../../component/editor/EditorComponent.vue'; | |
157 | -// API | |
158 | -import { saveBsrpProc, bsrpProc, updateBsrpProc } from '../../../../resources/api/bsrp'; | |
159 | - | |
160 | -export default { | |
161 | - components: { | |
162 | - PlusCircleFilled, CloseOutlined, | |
163 | - HrPopup, CorpCarPopup, CorpCardPopup, | |
164 | - SanctnList, EditorComponent, | |
165 | - }, | |
166 | - | |
167 | - data() { | |
168 | - return { | |
169 | - require: "/client/resources/img/require.png", | |
170 | - | |
171 | - pageId: null, | |
172 | - userInfo: this.$store.state.userInfo, | |
173 | - cmmnCodes: {}, | |
174 | - | |
175 | - isOpenNmprModal: false, | |
176 | - isOpenSanctnModal: false, | |
177 | - isOpenCardModal: false, | |
178 | - isOpenVhcleModal: false, | |
179 | - | |
180 | - // 출장정보 | |
181 | - bsrpInfo: { | |
182 | - applcntId: null, // 신청자 아이디 | |
183 | - bsrpSe: null, // 출장구분 | |
184 | - bsrpPlace: null, // 출장지 | |
185 | - bsrpPurps: null, // 출장목적 | |
186 | - bgnde: null, // 시작일 | |
187 | - beginHour: null, // 시작시 | |
188 | - beginMnt: null, // 시작분 | |
189 | - endde: null, // 종료일 | |
190 | - endHour: null, // 종료시 | |
191 | - endMnt: null, // 종료분 | |
192 | - bsrpNmprList: [], // 출장인원 목록 | |
193 | - }, | |
194 | - // 출장품의 | |
195 | - bsrpCnsul: { | |
196 | - bsrpId: null, // 출장 아이디 | |
197 | - cn: null, // 품의 내용 | |
198 | - confmAt: null, // 승인여부 | |
199 | - rgsde: null, // 등록일 | |
200 | - register: null, // 등록자 | |
201 | - updde: null, // 수정일 | |
202 | - updusr: null, // 수정자 | |
203 | - sanctnList: [], // 결재 목록 | |
204 | - }, | |
205 | - cprCardList: [], // 법인카드 목록 | |
206 | - cprVhcleList: [], // 법인차량 목록 | |
207 | - | |
208 | - totalDays: 0, // 일수 | |
209 | - }; | |
210 | - }, | |
211 | - | |
212 | - computed: {}, | |
213 | - | |
214 | - watch: { | |
215 | - cmmnCodes(newVal) { | |
216 | - if (Object.keys(newVal).length > 0) { | |
217 | - this.bsrpInfo.bsrpSe = this.cmmnCodes.bsrpCodeList[0].code; | |
218 | - } | |
219 | - }, | |
220 | - 'bsrpInfo.bgnde'(newVal, oldVal) { | |
221 | - if (newVal !== oldVal) { | |
222 | - this.calcDayCnt(); // 일수 계산 | |
223 | - } | |
224 | - }, | |
225 | - 'bsrpInfo.endde'(newVal, oldVal) { | |
226 | - if (newVal !== oldVal) { | |
227 | - this.calcDayCnt(); // 일수 계산 | |
228 | - } | |
229 | - }, | |
230 | - }, | |
231 | - | |
232 | - async created() { | |
233 | - this.pageId = this.$route.query.id; | |
234 | - this.cmmnCodes = await this.$defaultCodes(); | |
235 | - }, | |
236 | - | |
237 | - mounted() { | |
238 | - if (!this.$isEmpty(this.pageId)) { | |
239 | - this.findData(); | |
240 | - } | |
241 | - }, | |
242 | - | |
243 | - methods: { | |
244 | - // 상세 조회 | |
245 | - async findData() { | |
246 | - try { | |
247 | - const response = await bsrpProc(this.pageId); | |
248 | - const result = response.data.data; | |
249 | - | |
250 | - this.bsrpInfo = result.bsrpInfo; | |
251 | - this.bsrpInfo.bgnde = this.bsrpInfo.bgnde.split(' ')[0]; | |
252 | - this.bsrpInfo.endde = this.bsrpInfo.endde.split(' ')[0]; | |
253 | - | |
254 | - this.bsrpCnsul = result.bsrpCnsul; | |
255 | - this.cprCardList = result.cprCardList; | |
256 | - this.cprVhcleList = result.cprVhcleList; | |
257 | - } catch (error) { | |
258 | - console.error('데이터 조회 실패:', error); | |
259 | - alert(error.response.data.message); | |
260 | - | |
261 | - this.fnMoveTo('list'); | |
262 | - } | |
263 | - }, | |
264 | - | |
265 | - // 일수 계산 | |
266 | - calcDayCnt() { | |
267 | - if (!this.bsrpInfo.bgnde || !this.bsrpInfo.endde) { | |
268 | - this.totalDays = 0; | |
269 | - return; | |
270 | - } | |
271 | - | |
272 | - const startDate = new Date(this.bsrpInfo.bgnde); | |
273 | - const endDate = new Date(this.bsrpInfo.endde); | |
274 | - | |
275 | - if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { | |
276 | - this.totalDays = 0; | |
277 | - return; | |
278 | - } | |
279 | - | |
280 | - const timeDiff = endDate.getTime() - startDate.getTime(); | |
281 | - const dayDiff = Math.floor(timeDiff / (1000 * 60 * 60 * 24)) + 1; | |
282 | - | |
283 | - this.totalDays = Math.max(0, dayDiff); | |
284 | - }, | |
285 | - | |
286 | - // 동행자 추가 | |
287 | - fnAddNmpr(user) { | |
288 | - const data = { | |
289 | - triperId: user.userId, // 출장자 아이디 | |
290 | - deptId: user.deptId, // 부서 아이디 | |
291 | - clsf: user.clsf, // 직급 | |
292 | - | |
293 | - triperNm: user.userNm, // 출장자 이름 | |
294 | - deptNm: user.deptNm, // 부서 이름 | |
295 | - clsfNm: user.clsfNm, // 직급 이름 | |
296 | - }; | |
297 | - | |
298 | - this.bsrpInfo.bsrpNmprList.push(data); | |
299 | - this.isOpenNmprModal = false; | |
300 | - }, | |
301 | - | |
302 | - // 동행자 삭제 | |
303 | - fnDelNmpr(idx) { | |
304 | - this.bsrpInfo.bsrpNmprList.splice(idx, 1); | |
305 | - }, | |
306 | - | |
307 | - // 승인자 추가 | |
308 | - fnAddSanctn(user) { | |
309 | - const data = { | |
310 | - sanctnId: null, // 결재 아이디 | |
311 | - confmerId: user.userId, // 승인자 아이디 | |
312 | - clsf: user.clsf, // 직급 | |
313 | - sanctnOrdr: this.bsrpCnsul.sanctnList.length + 1, // 결재 순서 | |
314 | - sanctnIem: 'cnsul', // 결재 항목 | |
315 | - sanctnMbyId: null, // 결재 주체 아이디 | |
316 | - sanctnSe: this.cmmnCodes.sanctnCodeList[0].code, // 결재구분 | |
317 | - | |
318 | - confmerNm: user.userNm, // 승인자 이름 | |
319 | - clsfNm: user.clsfNm, // 직급 이름 | |
320 | - }; | |
321 | - | |
322 | - this.bsrpCnsul.sanctnList.push(data); | |
323 | - this.isOpenSanctnModal = false; | |
324 | - }, | |
325 | - | |
326 | - // 승인자 삭제 | |
327 | - fnDelSanctn(idx) { | |
328 | - this.bsrpInfo.sanctnList.splice(idx, 1); | |
329 | - this.bsrpInfo.sanctnList.forEach((item, idx) => { item.sanctnOrdr = idx + 1; }); | |
330 | - }, | |
331 | - | |
332 | - // 법인카드 추가 | |
333 | - fnAddCard(item) { | |
334 | - this.cprCardList.push(item); | |
335 | - this.isOpenCardModal = false; | |
336 | - }, | |
337 | - | |
338 | - // 법인카드 삭제 | |
339 | - fnDelCard(idx) { | |
340 | - this.cprCardList.splice(idx, 1); | |
341 | - }, | |
342 | - | |
343 | - // 법인차량 추가 | |
344 | - fnAddVhcle(item) { | |
345 | - item.drverId = ''; | |
346 | - this.cprVhcleList.push(item); | |
347 | - this.isOpenVhcleModal = false; | |
348 | - }, | |
349 | - | |
350 | - // 법인차량 삭제 | |
351 | - fnDelVhcle(idx) { | |
352 | - this.cprVhcleList.splice(idx, 1); | |
353 | - }, | |
354 | - | |
355 | - // 유효성 검사 | |
356 | - validateForm() { | |
357 | - if (this.$isEmpty(this.bsrpInfo.bsrpSe)) { | |
358 | - return false; | |
359 | - } | |
360 | - if (this.$isEmpty(this.bsrpInfo.bsrpPlace)) { | |
361 | - return false; | |
362 | - } | |
363 | - if (this.$isEmpty(this.bsrpInfo.bsrpPurps)) { | |
364 | - return false; | |
365 | - } | |
366 | - if (this.$isEmpty(this.bsrpInfo.bgnde) || this.$isEmpty(this.bsrpInfo.beginHour) || this.$isEmpty(this.bsrpInfo.beginMnt)) { | |
367 | - return false; | |
368 | - } | |
369 | - if (this.$isEmpty(this.bsrpInfo.endde) || this.$isEmpty(this.bsrpInfo.endHour) || this.$isEmpty(this.bsrpInfo.endMnt)) { | |
370 | - return false; | |
371 | - } | |
372 | - if (this.$isEmpty(this.bsrpInfo.bsrpNmprList) || this.bsrpInfo.bsrpNmprList.length === 0) { | |
373 | - return false; | |
374 | - } | |
375 | - if (this.$isEmpty(this.bsrpCnsul.cn)) { | |
376 | - return false; | |
377 | - } | |
378 | - | |
379 | - return true; | |
380 | - }, | |
381 | - | |
382 | - // 신청 | |
383 | - async fnSave() { | |
384 | - try { | |
385 | - if (!this.validateForm()) { | |
386 | - return; | |
387 | - } | |
388 | - | |
389 | - let data = { | |
390 | - bsrpCnsulInsertDTO: this.bsrpCnsul, | |
391 | - cardDtlsList: this.cprCardList, | |
392 | - vhcleDtlsList: this.cprVhcleList, | |
393 | - } | |
394 | - if (this.$isEmpty(this.pageId)) { | |
395 | - data.bsrpInfoInsertDTO = this.bsrpInfo; | |
396 | - } else { | |
397 | - data.bsrpInfoUpdateDTO = this.bsrpInfo; | |
398 | - } | |
399 | - | |
400 | - const response = this.$isEmpty(this.pageId) ? await saveBsrpProc(data) : await updateBsrpProc(data); | |
401 | - const message = this.$isEmpty(this.pageId) ? "등록되었습니다." : "수정되었습니다."; | |
402 | - alert(message); | |
403 | - | |
404 | - this.fnMoveTo('view', response.data.data.pageId); | |
405 | - } catch (error) { | |
406 | - console.error('저장 실패:', error); | |
407 | - const message = error.response?.data?.message || "저장에 실패했습니다."; | |
408 | - alert(message); | |
409 | - } | |
410 | - }, | |
411 | - | |
412 | - fnMoveTo(type, id) { | |
413 | - const routes = { | |
414 | - 'list': { name: 'ChuljangStatue' }, | |
415 | - 'view': { name: 'ChuljangDetailAll', query: { id } }, | |
416 | - 'edit': { name: 'ChuljangInsert', query: this.$isEmpty(id) ? {} : { id } }, | |
417 | - }; | |
418 | - | |
419 | - if (routes[type]) { | |
420 | - if (!this.$isEmpty(this.pageId) && type === 'list') { | |
421 | - this.$router.push({ name: 'ChuljangDetailAll', query: { id: this.pageId } }); | |
422 | - return; | |
423 | - } | |
424 | - this.$router.push(routes[type]); | |
425 | - } else { | |
426 | - alert("올바르지 않은 경로를 요청하여 목록으로 이동합니다."); | |
427 | - this.$router.push(routes['list']); | |
428 | - } | |
429 | - }, | |
430 | - }, | |
431 | -}; | |
432 | -</script>(파일 끝에 줄바꿈 문자 없음) |
--- client/views/pages/Manager/attendance/ChuljangStatue.vue
... | ... | @@ -1,142 +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="sch-form-wrap"> | |
7 | - <div class="input-group"> | |
8 | - <select class="form-select" v-model="request.year" @change="fnChangeCurrentPage(1)"> | |
9 | - <option value="">연도 전체</option> | |
10 | - <option v-for="year in years" :key="year" :value="year">{{ year }}년</option> | |
11 | - </select> | |
12 | - <select class="form-select" v-model="request.month" @change="fnChangeCurrentPage(1)"> | |
13 | - <option value="">월 전체</option> | |
14 | - <option v-for="month in months" :key="month" :value="month">{{ month }}월</option> | |
15 | - </select> | |
16 | - </div> | |
17 | - </div> | |
18 | - <div class="tbl-wrap"> | |
19 | - <table id="myTable" class="tbl data"> | |
20 | - <thead> | |
21 | - <tr> | |
22 | - <th>출장구분</th> | |
23 | - <th>출장지</th> | |
24 | - <th>목적</th> | |
25 | - <th>출장기간</th> | |
26 | - <th>품의서 상태</th> | |
27 | - <th>복명서 등록 여부</th> | |
28 | - <th>복명서 상태</th> | |
29 | - </tr> | |
30 | - </thead> | |
31 | - <tbody> | |
32 | - <tr v-for="(item, idx) in lists" :key="idx" :class="{ 'expired': item.hasRport }" @click="fnMoveTo('view', item.bsrpId)"> | |
33 | - <td>{{ item.bsrpSeNm }}</td> | |
34 | - <td>{{ item.bsrpPlace }}</td> | |
35 | - <td>{{ item.bsrpPurps }}</td> | |
36 | - <td>{{ item.bgnde }}</td> | |
37 | - <td>{{ item.bsrpCnsulDTO.confmAtNm }}</td> | |
38 | - <td>{{ item.hasRport ? '등록' : '미등록' }}</td> | |
39 | - <td>{{ item.hasRport ? item.bsrpRportDTO.confmAt : '-' }}</td> | |
40 | - </tr> | |
41 | - </tbody> | |
42 | - </table> | |
43 | - </div> | |
44 | - <Pagination :search="request" @onChange="fnChangeCurrentPage" /> | |
45 | - </div> | |
46 | - </div> | |
47 | - </div> | |
48 | -</template> | |
49 | -<script> | |
50 | -import { SearchOutlined } from '@ant-design/icons-vue'; | |
51 | -// API | |
52 | -import { bsrpsProc } from '../../../../resources/api/bsrp'; | |
53 | - | |
54 | -export default { | |
55 | - components: { | |
56 | - SearchOutlined | |
57 | - }, | |
58 | - | |
59 | - data() { | |
60 | - return { | |
61 | - photoicon: "/client/resources/img/photo_icon.png", | |
62 | - | |
63 | - years: [], | |
64 | - months: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], | |
65 | - | |
66 | - cmmnCodes: {}, | |
67 | - request: { | |
68 | - year: '', | |
69 | - month: '', | |
70 | - }, | |
71 | - lists: [], | |
72 | - }; | |
73 | - }, | |
74 | - | |
75 | - watch: {}, | |
76 | - | |
77 | - created() { | |
78 | - this.generateYears(); | |
79 | - }, | |
80 | - | |
81 | - mounted() { | |
82 | - this.findList(); // 목록 조회 | |
83 | - }, | |
84 | - | |
85 | - methods: { | |
86 | - generateYears() { | |
87 | - const startYear = 2020; | |
88 | - const currentYear = new Date().getFullYear(); | |
89 | - | |
90 | - for (let year = currentYear; year >= startYear; year--) { | |
91 | - this.years.push(year); | |
92 | - } | |
93 | - }, | |
94 | - | |
95 | - // 목록 조회 | |
96 | - async findList() { | |
97 | - try { | |
98 | - const response = await bsrpsProc(this.request); | |
99 | - const result = response.data.data; | |
100 | - | |
101 | - this.lists = result.lists; | |
102 | - this.request = result.search; | |
103 | - } catch (error) { | |
104 | - const message = error.response.data.message; | |
105 | - alert(message); | |
106 | - } | |
107 | - }, | |
108 | - | |
109 | - // 페이지 이동 | |
110 | - fnChangeCurrentPage(currentPage) { | |
111 | - this.request.currentPage = Number(currentPage); | |
112 | - this.$nextTick(() => { | |
113 | - this.findList(); | |
114 | - }); | |
115 | - }, | |
116 | - | |
117 | - fnMoveTo(type, id) { | |
118 | - const routes = { | |
119 | - 'list': { name: 'ChuljangStatue' }, | |
120 | - 'view': { name: 'ChuljangDetailAll', query: { id } }, | |
121 | - 'edit': { name: 'ChuljangInsert', query: this.$isEmpty(id) ? {} : { id } }, | |
122 | - }; | |
123 | - | |
124 | - if (routes[type]) { | |
125 | - if (!this.$isEmpty(this.pageId) && type === 'list') { | |
126 | - this.$router.push({ name: 'ChuljangDetailAll', query: { id: this.pageId } }); | |
127 | - return; | |
128 | - } | |
129 | - this.$router.push(routes[type]); | |
130 | - } else { | |
131 | - alert("올바르지 않은 경로를 요청하여 목록으로 이동합니다."); | |
132 | - this.$router.push(routes['list']); | |
133 | - } | |
134 | - }, | |
135 | - }, | |
136 | -}; | |
137 | -</script> | |
138 | -<style scoped> | |
139 | -tr { | |
140 | - cursor: pointer; | |
141 | -} | |
142 | -</style> |
--- client/views/pages/Manager/sanctn/ChuljangBokmyeong.vue
... | ... | @@ -1,181 +0,0 @@ |
1 | -<template> | |
2 | -<div class="card "> | |
3 | - <div class="card-body"> | |
4 | - <h2 class="card-title">승인 대기 목록</h2> | |
5 | - | |
6 | - <div class="form-card"> | |
7 | - <h1>출장복명서</h1> | |
8 | - <div class="approval-box tbl-wrap tbl2"> | |
9 | - <table class="tbl data"> | |
10 | - <tbody> | |
11 | - <tr class="thead"> | |
12 | - <td rowspan="2" class="th">승인자</td> | |
13 | - <td v-for="(approver, index) in approvers" :key="index"> | |
14 | - <p class="position">{{ approver.position }}</p> | |
15 | - </td> | |
16 | - </tr> | |
17 | - <tr> | |
18 | - <td v-for="(approver, index) in approvers" :key="index"> | |
19 | - <p class="name">{{ approver.name }}</p> | |
20 | - <p class="date">{{ approver.date }}</p> | |
21 | - </td> | |
22 | - </tr> | |
23 | - </tbody> | |
24 | - | |
25 | - </table> | |
26 | - </div> | |
27 | - <form class="row g-3 needs-validation detail" :class="{ 'was-validated': formSubmitted }" @submit.prevent="handleRegister" novalidate> | |
28 | - <div class="col-12 "> | |
29 | - <div class="col-12 border-x"> | |
30 | - <label for="youremail" class="form-label ">출장구분<p class="require"><img :src="require" alt=""></p></label> | |
31 | - <input v-model="email" type="text" name="username" class="form-control" readonly > | |
32 | - </div> | |
33 | - | |
34 | - <div class="col-12 border-x"> | |
35 | - <label for="yourPassword" class="form-label">이름</label> | |
36 | - <input v-model="password" type="password" name="password" class="form-control" readonly placeholder="주식회사 테이큰 소프트"> | |
37 | - </div> | |
38 | - </div> | |
39 | - <div class="col-12"> | |
40 | - <div class="col-12 border-x"> | |
41 | - <label for="youremail" class="form-label">부서</label> | |
42 | - <input v-model="email" type="text" name="username" class="form-control" readonly placeholder="과장"> | |
43 | - </div> | |
44 | - | |
45 | - <div class="col-12 border-x"> | |
46 | - <label for="yourPassword" class="form-label">직급</label> | |
47 | - <input v-model="password" type="password" name="password" class="form-control" readonly placeholder="팀장"> | |
48 | - </div> | |
49 | - </div> | |
50 | - <div class="col-12"> | |
51 | - <label for="yourName" class="form-label">출장지</label> | |
52 | - <input v-model="name" type="text" name="name" class="form-control" readonly> | |
53 | - </div> | |
54 | - <div class="col-12"> | |
55 | - <label for="yourName" class="form-label">출장목적</label> | |
56 | - <input v-model="name" type="text" name="name" class="form-control " readonly> | |
57 | - </div> | |
58 | - <div class="col-12"> | |
59 | - <label for="yourName" class="form-label">동행자</label> | |
60 | - <input v-model="name" type="text" name="name" class="form-control " readonly> | |
61 | - </div> | |
62 | - <div class="col-12 chuljang"> | |
63 | - <label for="yourName" class="form-label">복명내용</label> | |
64 | - <input v-model="name" type="text" name="name" class="form-control textarea " readonly> | |
65 | - </div> | |
66 | - <div class="col-12"> | |
67 | - <label for="yourName" class="form-label">법인카드</label> | |
68 | - <input v-model="name" type="text" name="name" class="form-control " readonly> | |
69 | - </div> | |
70 | - <div class="col-12"> | |
71 | - <label for="yourName" class="form-label">법인차량</label> | |
72 | - <input v-model="name" type="text" name="name" class="form-control " readonly> | |
73 | - </div> | |
74 | - <div class="col-12"> | |
75 | - <label for="yourName" class="form-label">여비계산</label> | |
76 | - <input v-model="name" type="text" name="name" class="form-control " readonly> | |
77 | - </div> | |
78 | - <div class="col-12 border-x"> | |
79 | - <label for="yourName" class="form-label">복명 신청일</label> | |
80 | - <input v-model="name" type="text" name="name" class="form-control" readonly placeholder="2025-01-01"> | |
81 | - </div> | |
82 | - | |
83 | - | |
84 | - </form> | |
85 | - </div> | |
86 | - <div class="buttons"> | |
87 | - <button class="btn sm primary" type="submit">승인</button> | |
88 | - <button class="btn sm btn-red" type="submit" @click="showPopup = true">반려</button> | |
89 | - <button class="btn sm tertiary " type="submit">목록</button> | |
90 | - </div> | |
91 | - <ReturnPopup v-if="showPopup" @close="showPopup = false" /> | |
92 | - | |
93 | - | |
94 | - </div> | |
95 | - </div> | |
96 | -</template> | |
97 | - | |
98 | -<script> | |
99 | -import ReturnPopup from '../../../component/Popup/ReturnPopup.vue'; | |
100 | -export default { | |
101 | - data() { | |
102 | - const today = new Date().toISOString().split('T')[0]; | |
103 | - return { | |
104 | - showPopup: false, | |
105 | - startDate: today, | |
106 | - startTime: "09:00", // 기본 시작 시간 09:00 | |
107 | - endDate: today, | |
108 | - endTime: "18:00", // 기본 종료 시간 18:00 | |
109 | - category: "", | |
110 | - dayCount: 1, | |
111 | - reason: "", // 사유 | |
112 | - approvers: [ | |
113 | - { position: '', name: '', date: '' }, | |
114 | - { position: '', name: '', date: '' }, | |
115 | - ], | |
116 | - listData: [ | |
117 | - { | |
118 | - type: '연차', | |
119 | - approvalType: '결재', | |
120 | - applicant: '홍길동', | |
121 | - period: '2025-05-10 ~ 2025-15-03', | |
122 | - requestDate: '2025-04-25', | |
123 | - status: '대기' | |
124 | - }, { | |
125 | - type: '반차', | |
126 | - approvalType: '전결', | |
127 | - applicant: '홍길동', | |
128 | - period: '2025-05-01 ~ 2025-05-03', | |
129 | - requestDate: '2025-04-25', | |
130 | - status: '승인' | |
131 | - }], | |
132 | - }; | |
133 | - }, | |
134 | - components: { | |
135 | - ReturnPopup | |
136 | - }, | |
137 | - computed: { | |
138 | - // Pinia Store의 상태를 가져옵니다. | |
139 | - loginUser() { | |
140 | - const authStore = useAuthStore(); | |
141 | - return authStore.getLoginUser; | |
142 | - }, | |
143 | - }, | |
144 | - methods: { | |
145 | - hasAnyApprover() { | |
146 | - return this.approvers.some( | |
147 | - (approver) => | |
148 | - approver.name?.trim() !== '' && approver.date?.trim() !== '' | |
149 | - ); | |
150 | - }, | |
151 | - calculateDayCount() { | |
152 | - const start = new Date(`${this.startDate}T${this.startTime}:00`); | |
153 | - const end = new Date(`${this.endDate}T${this.endTime}:00`); | |
154 | - | |
155 | - let totalHours = (end - start) / (1000 * 60 * 60); // 밀리초를 시간 단위로 변환 | |
156 | - | |
157 | - if (this.startDate !== this.endDate) { | |
158 | - // 시작일과 종료일이 다른경우 | |
159 | - const startDateObj = new Date(this.startDate); | |
160 | - const endDateObj = new Date(this.endDate); | |
161 | - const daysDifference = (endDateObj - startDateObj) / (1000 * 60 * 60 * 24); // 두 날짜 사이의 차이를 일수로 계산 | |
162 | - if (this.startTime !== "09:00" || this.endTime !== "18:00") { | |
163 | - this.dayCount = daysDifference + 0.5; // 시간 조건이 기준에서 벗어날 경우 | |
164 | - } else { | |
165 | - this.dayCount = Math.ceil(daysDifference + 1); // 시간 조건이 기준에 맞을 경우 | |
166 | - } | |
167 | - } else { | |
168 | - // 시작일과 종료일이 같은 경우 | |
169 | - if (this.startTime !== "09:00" || this.endTime !== "18:00") { | |
170 | - this.dayCount = 0.5; // 시작 시간 또는 종료 시간이 기준과 다를 경우 0.5 | |
171 | - } else { | |
172 | - this.dayCount = 1; // 기준 시간(09:00~18:00)이 맞으면 1일로 간주 | |
173 | - } | |
174 | - } | |
175 | - | |
176 | - }, | |
177 | - | |
178 | - | |
179 | - }, | |
180 | -}; | |
181 | -</script> |
--- client/views/pages/Manager/sanctn/ChuljangPumui.vue
... | ... | @@ -1,184 +0,0 @@ |
1 | -<template> | |
2 | - <div class="card "> | |
3 | - <div class="card-body"> | |
4 | - <h2 class="card-title">승인 대기 목록</h2> | |
5 | - | |
6 | - <div class="form-card"> | |
7 | - <h1>출장품의서</h1> | |
8 | - <div class="approval-box tbl-wrap tbl2"> | |
9 | - <table class="tbl data"> | |
10 | - <tbody> | |
11 | - <tr class="thead"> | |
12 | - <td rowspan="2" class="th">승인자</td> | |
13 | - <td v-for="(approver, index) in approvers" :key="index"> | |
14 | - <p class="position">{{ approver.position }}</p> | |
15 | - </td> | |
16 | - </tr> | |
17 | - <tr> | |
18 | - <td v-for="(approver, index) in approvers" :key="index"> | |
19 | - <p class="name">{{ approver.name }}</p> | |
20 | - <p class="date">{{ approver.date }}</p> | |
21 | - </td> | |
22 | - </tr> | |
23 | - | |
24 | - </tbody> | |
25 | - | |
26 | - </table> | |
27 | - </div> | |
28 | - <form class="row g-3 needs-validation detail" :class="{ 'was-validated': formSubmitted }" | |
29 | - @submit.prevent="handleRegister" novalidate> | |
30 | - <div class="col-12 "> | |
31 | - <div class="col-12 border-x"> | |
32 | - <label for="youremail" class="form-label ">출장구분<p class="require"><img :src="require" alt=""></p></label> | |
33 | - <input v-model="email" type="text" name="username" class="form-control" id="youremail" readonly> | |
34 | - </div> | |
35 | - | |
36 | - <div class="col-12 border-x"> | |
37 | - <label for="yourPassword" class="form-label">이름</label> | |
38 | - <input v-model="password" type="password" name="password" class="form-control" readonly | |
39 | - placeholder="주식회사 테이큰 소프트"> | |
40 | - </div> | |
41 | - </div> | |
42 | - <div class="col-12"> | |
43 | - <div class="col-12 border-x"> | |
44 | - <label for="youremail" class="form-label">부서</label> | |
45 | - <input v-model="email" type="text" name="username" class="form-control" readonly placeholder="과장"> | |
46 | - </div> | |
47 | - | |
48 | - <div class="col-12 border-x"> | |
49 | - <label for="yourPassword" class="form-label">직급</label> | |
50 | - <input v-model="password" type="password" name="password" class="form-control" readonly placeholder="팀장"> | |
51 | - </div> | |
52 | - </div> | |
53 | - <div class="col-12"> | |
54 | - <label for="yourName" class="form-label">출장지</label> | |
55 | - <input v-model="name" type="text" name="name" class="form-control" readonly> | |
56 | - </div> | |
57 | - <div class="col-12"> | |
58 | - <label for="yourName" class="form-label">출장목적</label> | |
59 | - <input v-model="name" type="text" name="name" class="form-control " readonly> | |
60 | - </div> | |
61 | - <div class="col-12"> | |
62 | - <label for="yourName" class="form-label">동행자</label> | |
63 | - <input v-model="name" type="text" name="name" class="form-control " readonly> | |
64 | - </div> | |
65 | - <div class="col-12 chuljang"> | |
66 | - <label for="yourName" class="form-label">내용</label> | |
67 | - <input v-model="name" type="text" name="name" class="form-control textarea " readonly> | |
68 | - </div> | |
69 | - <div class="col-12"> | |
70 | - <label for="yourName" class="form-label">법인카드</label> | |
71 | - <input v-model="name" type="text" name="name" class="form-control " readonly> | |
72 | - </div> | |
73 | - <div class="col-12"> | |
74 | - <label for="yourName" class="form-label">법인차량</label> | |
75 | - <input v-model="name" type="text" name="name" class="form-control " readonly> | |
76 | - </div> | |
77 | - <div class="col-12"> | |
78 | - <label for="yourName" class="form-label">품의 신청일</label> | |
79 | - <input v-model="name" type="text" name="name" class="form-control " readonly> | |
80 | - </div> | |
81 | - <div class="col-12 border-x return"> | |
82 | - <label for="yourName" class="form-label">반려사유</label> | |
83 | - <input v-model="name" type="text" name="name" class="form-control" readonly placeholder="2025-01-01"> | |
84 | - </div> | |
85 | - | |
86 | - | |
87 | - </form> | |
88 | - </div> | |
89 | - <div class="buttons"> | |
90 | - <button class="btn sm primary" type="submit">승인</button> | |
91 | - <button class="btn sm btn-red" type="submit" @click="showPopup = true">반려</button> | |
92 | - <button class="btn sm tertiary " type="submit">목록</button> | |
93 | - </div> | |
94 | - <ReturnPopup v-if="showPopup" @close="showPopup = false" /> | |
95 | - | |
96 | - </div> | |
97 | - </div> | |
98 | -</template> | |
99 | - | |
100 | -<script> | |
101 | -import ReturnPopup from '../../../component/Popup/ReturnPopup.vue'; | |
102 | -export default { | |
103 | - data() { | |
104 | - const today = new Date().toISOString().split('T')[0]; | |
105 | - return { | |
106 | - showPopup: false, | |
107 | - startDate: today, | |
108 | - startTime: "09:00", // 기본 시작 시간 09:00 | |
109 | - endDate: today, | |
110 | - endTime: "18:00", // 기본 종료 시간 18:00 | |
111 | - category: "", | |
112 | - dayCount: 1, | |
113 | - reason: "", // 사유 | |
114 | - approvers: [ | |
115 | - { position: '', name: '', date: '' }, | |
116 | - { position: '', name: '', date: '' }, | |
117 | - ], | |
118 | - listData: [ | |
119 | - { | |
120 | - type: '연차', | |
121 | - approvalType: '결재', | |
122 | - applicant: '홍길동', | |
123 | - period: '2025-05-10 ~ 2025-15-03', | |
124 | - requestDate: '2025-04-25', | |
125 | - status: '대기' | |
126 | - }, { | |
127 | - type: '반차', | |
128 | - approvalType: '전결', | |
129 | - applicant: '홍길동', | |
130 | - period: '2025-05-01 ~ 2025-05-03', | |
131 | - requestDate: '2025-04-25', | |
132 | - status: '승인' | |
133 | - }], | |
134 | - }; | |
135 | - }, | |
136 | - components: { | |
137 | - ReturnPopup | |
138 | - }, | |
139 | - computed: { | |
140 | - }, | |
141 | - methods: { | |
142 | - hasAnyApprover() { | |
143 | - return this.approvers.some( | |
144 | - (approver) => | |
145 | - approver.name?.trim() !== '' && approver.date?.trim() !== '' | |
146 | - ); | |
147 | - }, | |
148 | - calculateDayCount() { | |
149 | - const start = new Date(`${this.startDate}T${this.startTime}:00`); | |
150 | - const end = new Date(`${this.endDate}T${this.endTime}:00`); | |
151 | - | |
152 | - let totalHours = (end - start) / (1000 * 60 * 60); // 밀리초를 시간 단위로 변환 | |
153 | - | |
154 | - if (this.startDate !== this.endDate) { | |
155 | - // 시작일과 종료일이 다른경우 | |
156 | - const startDateObj = new Date(this.startDate); | |
157 | - const endDateObj = new Date(this.endDate); | |
158 | - const daysDifference = (endDateObj - startDateObj) / (1000 * 60 * 60 * 24); // 두 날짜 사이의 차이를 일수로 계산 | |
159 | - if (this.startTime !== "09:00" || this.endTime !== "18:00") { | |
160 | - this.dayCount = daysDifference + 0.5; // 시간 조건이 기준에서 벗어날 경우 | |
161 | - } else { | |
162 | - this.dayCount = Math.ceil(daysDifference + 1); // 시간 조건이 기준에 맞을 경우 | |
163 | - } | |
164 | - } else { | |
165 | - // 시작일과 종료일이 같은 경우 | |
166 | - if (this.startTime !== "09:00" || this.endTime !== "18:00") { | |
167 | - this.dayCount = 0.5; // 시작 시간 또는 종료 시간이 기준과 다를 경우 0.5 | |
168 | - } else { | |
169 | - this.dayCount = 1; // 기준 시간(09:00~18:00)이 맞으면 1일로 간주 | |
170 | - } | |
171 | - } | |
172 | - | |
173 | - }, | |
174 | - | |
175 | - | |
176 | - | |
177 | - }, | |
178 | -}; | |
179 | -</script> | |
180 | -<style scoped> | |
181 | -td p { | |
182 | - width: 125px; | |
183 | -} | |
184 | -</style> |
--- client/views/pages/Manager/sanctn/Hyuga.vue
... | ... | @@ -1,152 +0,0 @@ |
1 | -<template> | |
2 | -<div class="card "> | |
3 | - <div class="card-body"> | |
4 | - <h2 class="card-title">승인 대기 목록</h2> | |
5 | - | |
6 | - <div class="form-card"> | |
7 | - <h1>휴가신청서</h1> | |
8 | - <div class="approval-box tbl-wrap tbl2"> | |
9 | - <table class="tbl data"> | |
10 | - <tbody> | |
11 | - <tr class="thead"> | |
12 | - <td rowspan="2" class="th">승인자</td> | |
13 | - <td>과장</td> | |
14 | - <td>소장</td> | |
15 | - </tr> | |
16 | - <tr> | |
17 | - <td><p class="name">홍길동</p><p class="date">2025/05/09</p></td> | |
18 | - <td><p class="name">홍길동</p><p class="date">2025/05/09</p></td> | |
19 | - </tr> | |
20 | - </tbody> | |
21 | - | |
22 | - </table> | |
23 | - </div> | |
24 | - <form class="row g-3 needs-validation detail" :class="{ 'was-validated': formSubmitted }" @submit.prevent="handleRegister" novalidate> | |
25 | - <div class="col-12 "> | |
26 | - <div class="col-12 border-x"> | |
27 | - <label for="youremail" class="form-label ">유형<p class="require"><img :src="require" alt=""></p></label> | |
28 | - <input v-model="email" type="text" name="username" class="form-control" readonly > | |
29 | - </div> | |
30 | - | |
31 | - <div class="col-12 border-x"> | |
32 | - <label for="yourPassword" class="form-label">신청자</label> | |
33 | - <input v-model="password" type="password" name="password" class="form-control" readonly placeholder="주식회사 테이큰 소프트"> | |
34 | - </div> | |
35 | - </div> | |
36 | - <div class="col-12"> | |
37 | - <div class="col-12 border-x"> | |
38 | - <label for="youremail" class="form-label">부서</label> | |
39 | - <input v-model="email" type="text" name="username" class="form-control" readonly placeholder="과장"> | |
40 | - </div> | |
41 | - | |
42 | - <div class="col-12 border-x"> | |
43 | - <label for="yourPassword" class="form-label">직급</label> | |
44 | - <input v-model="password" type="password" name="password" class="form-control" readonly placeholder="팀장"> | |
45 | - </div> | |
46 | - </div> | |
47 | - <div class="col-12"> | |
48 | - <label for="yourName" class="form-label">기간</label> | |
49 | - <input v-model="name" type="text" name="name" class="form-control" readonly> | |
50 | - </div> | |
51 | - <div class="col-12 hyuga"> | |
52 | - <label for="yourName" class="form-label">세부사항</label> | |
53 | - <input v-model="name" type="text" name="name" class="form-control textarea" readonly> | |
54 | - </div> | |
55 | - <div class="col-12 "> | |
56 | - <label for="yourName" class="form-label">신청일</label> | |
57 | - <input v-model="name" type="text" name="name" class="form-control " readonly> | |
58 | - </div> | |
59 | - <div class="col-12 border-x return" > | |
60 | - <label for="yourName" class="form-label ">반려사유</label> | |
61 | - <input v-model="name" type="text" name="name" class="form-control" readonly > | |
62 | - </div> | |
63 | - | |
64 | - | |
65 | - </form> | |
66 | - </div> | |
67 | - <div class="buttons"> | |
68 | - <button class="btn sm primary" type="button">승인</button> | |
69 | - <button class="btn sm btn-red" type="button" @click="showPopup = true">반려</button> | |
70 | - <button class="btn sm tertiary " type="button">목록</button> | |
71 | - </div> | |
72 | - <ReturnPopup v-if="showPopup" @close="showPopup = false"/> | |
73 | - </div> | |
74 | - </div> | |
75 | -</template> | |
76 | - | |
77 | -<script> | |
78 | -import ReturnPopup from '../../../component/Popup/ReturnPopup.vue'; | |
79 | -export default { | |
80 | - data() { | |
81 | - const today = new Date().toISOString().split('T')[0]; | |
82 | - return { | |
83 | - showPopup: false, | |
84 | - startDate: today, | |
85 | - startTime: "09:00", // 기본 시작 시간 09:00 | |
86 | - endDate: today, | |
87 | - endTime: "18:00", // 기본 종료 시간 18:00 | |
88 | - category: "", | |
89 | - dayCount: 1, | |
90 | - reason: "", // 사유 | |
91 | - listData: [ | |
92 | - { | |
93 | - type: '연차', | |
94 | - approvalType: '결재', | |
95 | - applicant: '홍길동', | |
96 | - period: '2025-05-10 ~ 2025-15-03', | |
97 | - requestDate: '2025-04-25', | |
98 | - status: '대기' | |
99 | - }, { | |
100 | - type: '반차', | |
101 | - approvalType: '전결', | |
102 | - applicant: '홍길동', | |
103 | - period: '2025-05-01 ~ 2025-05-03', | |
104 | - requestDate: '2025-04-25', | |
105 | - status: '승인' | |
106 | - }], | |
107 | - }; | |
108 | - }, | |
109 | - components: { | |
110 | - ReturnPopup | |
111 | - }, | |
112 | - computed: { | |
113 | - | |
114 | - }, | |
115 | - methods: { | |
116 | - | |
117 | - calculateDayCount() { | |
118 | - const start = new Date(`${this.startDate}T${this.startTime}:00`); | |
119 | - const end = new Date(`${this.endDate}T${this.endTime}:00`); | |
120 | - | |
121 | - let totalHours = (end - start) / (1000 * 60 * 60); // 밀리초를 시간 단위로 변환 | |
122 | - | |
123 | - if (this.startDate !== this.endDate) { | |
124 | - // 시작일과 종료일이 다른경우 | |
125 | - const startDateObj = new Date(this.startDate); | |
126 | - const endDateObj = new Date(this.endDate); | |
127 | - const daysDifference = (endDateObj - startDateObj) / (1000 * 60 * 60 * 24); // 두 날짜 사이의 차이를 일수로 계산 | |
128 | - if (this.startTime !== "09:00" || this.endTime !== "18:00") { | |
129 | - this.dayCount = daysDifference + 0.5; // 시간 조건이 기준에서 벗어날 경우 | |
130 | - } else { | |
131 | - this.dayCount = Math.ceil(daysDifference + 1); // 시간 조건이 기준에 맞을 경우 | |
132 | - } | |
133 | - } else { | |
134 | - // 시작일과 종료일이 같은 경우 | |
135 | - if (this.startTime !== "09:00" || this.endTime !== "18:00") { | |
136 | - this.dayCount = 0.5; // 시작 시간 또는 종료 시간이 기준과 다를 경우 0.5 | |
137 | - } else { | |
138 | - this.dayCount = 1; // 기준 시간(09:00~18:00)이 맞으면 1일로 간주 | |
139 | - } | |
140 | - } | |
141 | - | |
142 | - this.validateForm(); // dayCount 변경 후 폼 재검증 | |
143 | - }, | |
144 | - | |
145 | - | |
146 | - | |
147 | - }, | |
148 | - mounted() { | |
149 | - }, | |
150 | - | |
151 | -}; | |
152 | -</script> |
--- client/views/pages/Manager/sanctn/PendingApprovalList.vue
+++ client/views/pages/Manager/sanctn/PendingApprovalList.vue
... | ... | @@ -1,118 +1,117 @@ |
1 | 1 |
<template> |
2 |
- |
|
3 |
- <div class="card" style="height: 100%;"> |
|
4 |
- <div class="card-body"> |
|
5 |
- <div> |
|
6 |
- <h2 class="card-title">승인 대기 목록</h2> |
|
7 |
- <div style="height: 50%;"> |
|
8 |
- <div class="d-flex justify-between align-center"> |
|
9 |
- <h3 class="sub-title">승인 대기</h3> |
|
10 |
- <div class="search-wrap mb10"> |
|
11 |
- <select class="form-select sm" v-model="pendingSearchParams.year" @change="handlePageChange(1, 'pending')"> |
|
12 |
- <option value="">연도 전체</option> |
|
13 |
- <option v-for="year in yearOptions" :key="year" :value="year">{{ year }}년</option> |
|
14 |
- </select> |
|
15 |
- <select class="form-select sm" v-model="pendingSearchParams.month" @change="handlePageChange(1, 'pending')"> |
|
16 |
- <option value="">월 전체</option> |
|
17 |
- <option v-for="month in monthOptions" :key="month" :value="month">{{ month }}월</option> |
|
18 |
- </select> |
|
19 |
- <input type="text" class="form-control sm" v-model="pendingSearchParams.searchText" placeholder="검색어를 입력하세요." @keyup.enter="handleSearch('pending')"> |
|
20 |
- <button button="button" class="ico-sch" @click="handleSearch('pending')"> |
|
21 |
- <SearchOutlined /> |
|
22 |
- </button> |
|
23 |
- </div> |
|
2 |
+ <div class="card" style="height: 100%;"> |
|
3 |
+ <div class="card-body"> |
|
4 |
+ <div> |
|
5 |
+ <h2 class="card-title">승인 대기 목록</h2> |
|
6 |
+ <div style="height: 50%;"> |
|
7 |
+ <div class="d-flex justify-between align-center"> |
|
8 |
+ <h3 class="sub-title">승인 대기</h3> |
|
9 |
+ <div class="search-wrap mb10"> |
|
10 |
+ <select class="form-select sm" v-model="pendingSearchParams.year" @change="handlePageChange(1, 'pending')"> |
|
11 |
+ <option value="">연도 전체</option> |
|
12 |
+ <option v-for="year in yearOptions" :key="year" :value="year">{{ year }}년</option> |
|
13 |
+ </select> |
|
14 |
+ <select class="form-select sm" v-model="pendingSearchParams.month" @change="handlePageChange(1, 'pending')"> |
|
15 |
+ <option value="">월 전체</option> |
|
16 |
+ <option v-for="month in monthOptions" :key="month" :value="month">{{ month }}월</option> |
|
17 |
+ </select> |
|
18 |
+ <input type="text" class="form-control sm" v-model="pendingSearchParams.searchText" placeholder="검색어를 입력하세요." @keyup.enter="handleSearch('pending')"> |
|
19 |
+ <button button="button" class="ico-sch" @click="handleSearch('pending')"> |
|
20 |
+ <SearchOutlined /> |
|
21 |
+ </button> |
|
24 | 22 |
</div> |
25 |
- <div class="tbl-wrap mb20"> |
|
26 |
- <table id="myTable" class="tbl data common-radius"> |
|
27 |
- <colgroup> |
|
28 |
- <col style="width: 13.33%;"> |
|
29 |
- <col style="width: 13.33%;"> |
|
30 |
- <col style="width: 13.33%;"> |
|
31 |
- <col style="width: 30%;"> |
|
32 |
- <col style="width: 30%;"> |
|
33 |
- </colgroup> |
|
34 |
- <thead> |
|
35 |
- <tr> |
|
36 |
- <th style="border-radius: 1rem 0 0 0;">구분</th> |
|
37 |
- <th>결재구분</th> |
|
38 |
- <th>신청자</th> |
|
39 |
- <th>기간</th> |
|
40 |
- <th style="border-radius: 0 1rem 0 0;">신청일</th> |
|
41 |
- </tr> |
|
42 |
- </thead> |
|
43 |
- <tbody> |
|
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 |
- </tbody> |
|
52 |
- </table> |
|
53 |
- </div> |
|
54 |
- <Pagenation :search="pendingSearchParams" @onChange="(currentPage) => handlePageChange(currentPage, 'pending')" /> |
|
55 | 23 |
</div> |
56 |
- <div style="height: 50%;"> |
|
57 |
- <div class="d-flex justify-between align-center"> |
|
58 |
- <h3 class="sub-title">승인 이력</h3> |
|
59 |
- <div class="search-wrap mb10"> |
|
60 |
- <select class="form-select sm" v-model="historySearchParams.year" @change="handlePageChange(1, 'history')"> |
|
61 |
- <option value="">연도 전체</option> |
|
62 |
- <option v-for="year in yearOptions" :key="year" :value="year">{{ year }}년</option> |
|
63 |
- </select> |
|
64 |
- <select class="form-select sm" v-model="historySearchParams.month" @change="handlePageChange(1, 'history')"> |
|
65 |
- <option value="">월 전체</option> |
|
66 |
- <option v-for="month in monthOptions" :key="month" :value="month">{{ month }}월</option> |
|
67 |
- </select> |
|
68 |
- <select name="" id="" class="form-select sm"> |
|
69 |
- <option value="all">상태</option> |
|
70 |
- <option value="">승인</option> |
|
71 |
- <option value="">반려</option> |
|
72 |
- </select> |
|
73 |
- <input type="text" class="form-control sm" v-model="historySearchParams.searchText" placeholder="검색어를 입력하세요." @keyup.enter="handleSearch('history')"> |
|
74 |
- <button button="button" class="ico-sch" @click="handleSearch('history')"> |
|
75 |
- <SearchOutlined /> |
|
76 |
- </button> |
|
77 |
- </div> |
|
78 |
- </div> |
|
79 |
- <div class="tbl-wrap mb20"> |
|
80 |
- <table id="myTable" class="tbl data"> |
|
81 |
- <colgroup> |
|
82 |
- <col style="width: 13.33%;"> |
|
83 |
- <col style="width: 13.33%;"> |
|
84 |
- <col style="width: 13.33%;"> |
|
85 |
- <col style="width: 30%;"> |
|
86 |
- <col style="width: 16.66%;"> |
|
87 |
- <col style="width: 13.33%;"> |
|
88 |
- </colgroup> |
|
89 |
- <thead> |
|
90 |
- <tr> |
|
91 |
- <th style="border-radius: 1rem 0 0 0;">구분</th> |
|
92 |
- <th>결재구분</th> |
|
93 |
- <th>신청자</th> |
|
94 |
- <th>기간</th> |
|
95 |
- <th>신청일</th> |
|
96 |
- <th style="border-radius: 0 1rem 0 0;">상태</th> |
|
97 |
- </tr> |
|
98 |
- </thead> |
|
99 |
- <tbody> |
|
100 |
- <tr v-for="(item, index) in approvalHistoryList" :key="index" class="expired" @click="handleDetailNavigation(item.sanctnMbyId)"> |
|
101 |
- <td>{{ item.sanctnIemNm }}</td> |
|
102 |
- <td>{{ item.sanctnSeNm }}</td> |
|
103 |
- <td>{{ item.registerNm }}</td> |
|
104 |
- <td>{{ $formattedDates(item) }}</td> |
|
105 |
- <td>{{ item.rgsde }}</td> |
|
106 |
- <td>{{ item.confmAtNm }}</td> |
|
107 |
- </tr> |
|
108 |
- </tbody> |
|
109 |
- </table> |
|
110 |
- </div> |
|
111 |
- <Pagenation :search="historySearchParams" @onChange="(currentPage) => handlePageChange(currentPage, 'history')" /> |
|
24 |
+ <div class="tbl-wrap mb20"> |
|
25 |
+ <table id="myTable" class="tbl data common-radius"> |
|
26 |
+ <colgroup> |
|
27 |
+ <col style="width: 13.33%;"> |
|
28 |
+ <col style="width: 13.33%;"> |
|
29 |
+ <col style="width: 13.33%;"> |
|
30 |
+ <col style="width: 30%;"> |
|
31 |
+ <col style="width: 30%;"> |
|
32 |
+ </colgroup> |
|
33 |
+ <thead> |
|
34 |
+ <tr> |
|
35 |
+ <th style="border-radius: 1rem 0 0 0;">구분</th> |
|
36 |
+ <th>결재구분</th> |
|
37 |
+ <th>신청자</th> |
|
38 |
+ <th>기간</th> |
|
39 |
+ <th style="border-radius: 0 1rem 0 0;">신청일</th> |
|
40 |
+ </tr> |
|
41 |
+ </thead> |
|
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> |
|
49 |
+ </tr> |
|
50 |
+ </tbody> |
|
51 |
+ </table> |
|
112 | 52 |
</div> |
53 |
+ <Pagenation :search="pendingSearchParams" @onChange="(currentPage) => handlePageChange(currentPage, 'pending')" /> |
|
54 |
+ </div> |
|
55 |
+ <div style="height: 50%;"> |
|
56 |
+ <div class="d-flex justify-between align-center"> |
|
57 |
+ <h3 class="sub-title">승인 이력</h3> |
|
58 |
+ <div class="search-wrap mb10"> |
|
59 |
+ <select class="form-select sm" v-model="historySearchParams.year" @change="handlePageChange(1, 'history')"> |
|
60 |
+ <option value="">연도 전체</option> |
|
61 |
+ <option v-for="year in yearOptions" :key="year" :value="year">{{ year }}년</option> |
|
62 |
+ </select> |
|
63 |
+ <select class="form-select sm" v-model="historySearchParams.month" @change="handlePageChange(1, 'history')"> |
|
64 |
+ <option value="">월 전체</option> |
|
65 |
+ <option v-for="month in monthOptions" :key="month" :value="month">{{ month }}월</option> |
|
66 |
+ </select> |
|
67 |
+ <select name="" id="" class="form-select sm"> |
|
68 |
+ <option value="all">상태</option> |
|
69 |
+ <option value="">승인</option> |
|
70 |
+ <option value="">반려</option> |
|
71 |
+ </select> |
|
72 |
+ <input type="text" class="form-control sm" v-model="historySearchParams.searchText" placeholder="검색어를 입력하세요." @keyup.enter="handleSearch('history')"> |
|
73 |
+ <button button="button" class="ico-sch" @click="handleSearch('history')"> |
|
74 |
+ <SearchOutlined /> |
|
75 |
+ </button> |
|
76 |
+ </div> |
|
77 |
+ </div> |
|
78 |
+ <div class="tbl-wrap mb20"> |
|
79 |
+ <table id="myTable" class="tbl data"> |
|
80 |
+ <colgroup> |
|
81 |
+ <col style="width: 13.33%;"> |
|
82 |
+ <col style="width: 13.33%;"> |
|
83 |
+ <col style="width: 13.33%;"> |
|
84 |
+ <col style="width: 30%;"> |
|
85 |
+ <col style="width: 16.66%;"> |
|
86 |
+ <col style="width: 13.33%;"> |
|
87 |
+ </colgroup> |
|
88 |
+ <thead> |
|
89 |
+ <tr> |
|
90 |
+ <th style="border-radius: 1rem 0 0 0;">구분</th> |
|
91 |
+ <th>결재구분</th> |
|
92 |
+ <th>신청자</th> |
|
93 |
+ <th>기간</th> |
|
94 |
+ <th>신청일</th> |
|
95 |
+ <th style="border-radius: 0 1rem 0 0;">상태</th> |
|
96 |
+ </tr> |
|
97 |
+ </thead> |
|
98 |
+ <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> |
|
106 |
+ </tr> |
|
107 |
+ </tbody> |
|
108 |
+ </table> |
|
109 |
+ </div> |
|
110 |
+ <Pagenation :search="historySearchParams" @onChange="(currentPage) => handlePageChange(currentPage, 'history')" /> |
|
113 | 111 |
</div> |
114 | 112 |
</div> |
115 | 113 |
</div> |
114 |
+ </div> |
|
116 | 115 |
</template> |
117 | 116 |
<script> |
118 | 117 |
import { SearchOutlined } from '@ant-design/icons-vue'; |
... | ... | @@ -230,7 +229,7 @@ |
230 | 229 |
if (approvalType === "VCATN") { |
231 | 230 |
this.$router.push({ name: 'HyugaDetail', query: { id, type: 'sanctns' } }); |
232 | 231 |
} if (approvalType === "BSRP") { |
233 |
- this.$router.push({ name: 'ChuljangDetailAll', query: { id, type: 'sanctns' } }); |
|
232 |
+ this.$router.push({ name: 'BsrpViewPage', query: { id, type: 'sanctns' } }); |
|
234 | 233 |
} |
235 | 234 |
}, |
236 | 235 |
|
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?