
+++ client/resources/img/start-sm.png
Binary file is not shown |
+++ client/resources/img/stop-sm.png
Binary file is not shown |
--- client/resources/scss/admin/content.scss
+++ client/resources/scss/admin/content.scss
... | ... | @@ -33,6 +33,7 @@ |
33 | 33 |
} |
34 | 34 |
} |
35 | 35 |
} |
36 |
+ |
|
36 | 37 |
} |
37 | 38 |
.sidemenu{ |
38 | 39 |
width: 350px; |
... | ... | @@ -151,14 +152,36 @@ |
151 | 152 |
} |
152 | 153 |
.name{color: #000;} |
153 | 154 |
} |
154 |
- h2{ background: #213F9A; color: #fff; border-radius: 10px; font-size: 17px; text-align: left; padding: 10px 15px;} |
|
155 |
+ details[open] > summary { |
|
156 |
+ |
|
157 |
+ background: #213F9A; |
|
158 |
+ color: #fff; |
|
159 |
+ |
|
160 |
+ .icon{display: block;} |
|
161 |
+ } |
|
162 |
+ |
|
155 | 163 |
} |
156 | 164 |
.menu-box{ |
165 |
+ summary{ |
|
166 |
+ cursor: pointer; |
|
167 |
+ border: 1px solid #213F9A; |
|
168 |
+ color: #000; |
|
169 |
+ display: flex; |
|
170 |
+ justify-content: space-between; |
|
171 |
+ font-size: 17px; |
|
172 |
+ text-align: left; |
|
173 |
+ padding: 10px 15px; |
|
174 |
+ border-radius: 10px; |
|
175 |
+ .icon{display: none;} |
|
176 |
+ } |
|
177 |
+ |
|
157 | 178 |
margin-top: 20px; |
179 |
+ ul{padding: 10px 10px 0px 10px;} |
|
158 | 180 |
li{ |
159 | 181 |
margin-bottom: 10px; |
160 | 182 |
a{display: flex; justify-content: space-between;} |
161 |
- a:focus{outline: 0;} |
|
183 |
+ |
|
184 |
+ |
|
162 | 185 |
} |
163 | 186 |
} |
164 | 187 |
.boxs{ |
... | ... | @@ -246,6 +269,7 @@ |
246 | 269 |
.content{ |
247 | 270 |
.pagination{ |
248 | 271 |
margin-top: 20px; |
272 |
+ margin-bottom: 5rem; |
|
249 | 273 |
ul { |
250 | 274 |
display: flex; |
251 | 275 |
list-style: none; |
... | ... | @@ -301,7 +325,7 @@ |
301 | 325 |
} |
302 | 326 |
.top-content{display: flex; justify-content: space-between; gap: 40px; margin-bottom: 15px;} |
303 | 327 |
.boxs{ |
304 |
- width: 550px; |
|
328 |
+ // width: 550px; |
|
305 | 329 |
|
306 | 330 |
.blue-box{ |
307 | 331 |
background-color: #D7DCF1; |
... | ... | @@ -362,16 +386,93 @@ |
362 | 386 |
} |
363 | 387 |
} |
364 | 388 |
.card{ |
365 |
- |
|
389 |
+ .color-boxs{ |
|
390 |
+ display: flex; |
|
391 |
+ justify-content: space-between; |
|
392 |
+ gap: 10px; |
|
393 |
+ margin-bottom: 20px; |
|
394 |
+ .box{ |
|
395 |
+ display: block; |
|
396 |
+ text-align: center; |
|
397 |
+ width: calc(100% / 6); |
|
398 |
+ background-color: #F9F9F9; |
|
399 |
+ font-size: 30px; |
|
400 |
+ font-weight: 700; |
|
401 |
+ border-radius: 10px; |
|
402 |
+ padding-bottom: 24px; |
|
403 |
+ h3{background-color: #333333; color: #fff; border-radius: 10px;width: 100%; height: 50px; |
|
404 |
+ line-height: 50px; |
|
405 |
+ margin-bottom: 17px; font-size: 20px;} |
|
406 |
+ } |
|
407 |
+ .box.red{ |
|
408 |
+ background-color: #FEF3F3; |
|
409 |
+ h3{background-color: #E92727;} |
|
410 |
+ |
|
411 |
+ } |
|
412 |
+ .box.green{ |
|
413 |
+ background-color: #EFF9FB; |
|
414 |
+ h3{background-color: #3C97AB;} |
|
415 |
+ } |
|
416 |
+ .box.blue{ |
|
417 |
+ background-color: #E9EFF8; |
|
418 |
+ h3{background-color: #1D75E1;} |
|
419 |
+ } |
|
420 |
+ .box.purple{ |
|
421 |
+ background-color: #F5EFFA; |
|
422 |
+ h3{background-color: #A36CD4;} |
|
423 |
+ } |
|
424 |
+ .box.orange{ |
|
425 |
+ background-color: #FFF8F3; |
|
426 |
+ h3{background-color: #F7941C;} |
|
427 |
+ } |
|
428 |
+ |
|
429 |
+ } |
|
430 |
+ .name-box{ |
|
431 |
+ margin-bottom: 20px; |
|
432 |
+ .img-area{ |
|
433 |
+ flex-shrink: 0; |
|
434 |
+ margin-right: 30px; |
|
435 |
+ width: 300px; |
|
436 |
+ height: 150px; |
|
437 |
+ background-color: #EFF1FA; |
|
438 |
+ border-radius: 20px; |
|
439 |
+ text-align: center; |
|
440 |
+ padding: 20px; |
|
441 |
+ .img{ |
|
442 |
+ display: flex; |
|
443 |
+ justify-self: center; |
|
444 |
+ background-color: #fff; |
|
445 |
+ width: 92px; |
|
446 |
+ height: 110px; |
|
447 |
+ padding: 8px; |
|
448 |
+ img{object-fit: cover; width: 100%; |
|
449 |
+ height: 100%;} |
|
450 |
+ } |
|
451 |
+ } |
|
452 |
+ form{ |
|
453 |
+ input[readonly] { |
|
454 |
+ border-color: #fff; |
|
455 |
+ background-color: #fff !important; |
|
456 |
+ cursor: context-menu; |
|
457 |
+ } |
|
458 |
+ .col-12 { |
|
459 |
+ input{margin: 4px 10px;} |
|
460 |
+ label{line-height: 48px;} |
|
461 |
+ } |
|
462 |
+ } |
|
463 |
+ |
|
464 |
+ } |
|
366 | 465 |
.sch-form-wrap { |
367 | 466 |
justify-self: end; |
368 |
- margin: 20px 0 ; |
|
467 |
+ margin-bottom: 20px; |
|
369 | 468 |
.input-group{ |
370 | 469 |
.form-select, .form-control{ |
371 | 470 |
height: 40px; |
372 | 471 |
border-color: #C7CFE3; |
373 | 472 |
font-size: 16px; |
374 | 473 |
} |
474 |
+ .form-control{padding-right: 30px; padding-left: 10px;} |
|
475 |
+ .ico-sch{right: 5px;} |
|
375 | 476 |
} |
376 | 477 |
|
377 | 478 |
} |
... | ... | @@ -380,11 +481,15 @@ |
380 | 481 |
img{margin: 5px 5px 0 0;} |
381 | 482 |
} |
382 | 483 |
.input-group{width: auto; |
383 |
- .form-control{padding-right: 30px;} |
|
384 |
- .ico-sch{right: 5px;} |
|
484 |
+ |
|
385 | 485 |
} |
386 | 486 |
} |
387 | 487 |
.tbl-wrap{ |
488 |
+ table.buseo{ |
|
489 |
+ tbody{ |
|
490 |
+ tr{cursor: pointer;} |
|
491 |
+ } |
|
492 |
+ } |
|
388 | 493 |
th,td{text-align: center !important; font-size: 16px !important;} |
389 | 494 |
.status-approved { |
390 | 495 |
color: #1D75E1; |
... | ... | @@ -406,11 +511,28 @@ |
406 | 511 |
img{margin-top: 9px;} |
407 | 512 |
} |
408 | 513 |
.card-body{ |
409 |
- |
|
514 |
+ .flex{display: flex; align-items: center;} |
|
515 |
+ .flex.sb{justify-content: space-between;} |
|
516 |
+ .card-title{ |
|
517 |
+ margin-bottom: 20px; |
|
518 |
+ } |
|
519 |
+ .sub{ |
|
520 |
+ margin-right: 20px; |
|
521 |
+ img{width: 50px; height: 50px;} |
|
522 |
+ .date{margin-left: 10px;} |
|
523 |
+ } |
|
524 |
+ .sch-wrap{ |
|
525 |
+ border: 1px solid #213F9A; |
|
526 |
+ padding: 20px; |
|
527 |
+ border-radius: 10px; |
|
528 |
+ margin-bottom: 20px; |
|
529 |
+ .buttons{gap: 5px; margin-top: 0;} |
|
530 |
+ .sch-form-wrap{margin-bottom: 0;} |
|
531 |
+ } |
|
410 | 532 |
form{ |
411 |
- margin-bottom:5rem; |
|
533 |
+ |
|
412 | 534 |
border: 1px solid #C7CFE3; |
413 |
- border-radius: 10px; overflow: hidden;} |
|
535 |
+ border-radius: 10px; overflow: hidden;} |
|
414 | 536 |
.col-12{ |
415 | 537 |
width: 100%; |
416 | 538 |
display: flex; |
... | ... | @@ -418,9 +540,10 @@ |
418 | 540 |
label{width: 140px ;background: #EFF1FA; font-weight: 600; font-size: 16px; text-align: center; line-height: 70px; flex-shrink: 0; position: relative;} |
419 | 541 |
p.require{ |
420 | 542 |
position: absolute; |
421 |
- right: 44px; |
|
422 |
- top: 26px; |
|
543 |
+ right: 37px; |
|
544 |
+ top: 23px; |
|
423 | 545 |
} |
546 |
+ .d-flex{display: flex; flex-shrink: 0;} |
|
424 | 547 |
select, input{margin: 15px 10px; border-color: #DDDDDD; height: var(--tk-input-h-sm);} |
425 | 548 |
.form-control[readonly]{background-color: #EFF1FA;} |
426 | 549 |
.invalid-feedback{color: #E92727; font-size: 13px;} |
... | ... | @@ -429,11 +552,99 @@ |
429 | 552 |
input{margin: 0;} |
430 | 553 |
p{margin-top: 5px;} |
431 | 554 |
} |
555 |
+ .approval-container{ |
|
556 |
+ display: flex; |
|
557 |
+ flex-direction: column; |
|
558 |
+ gap: 8px; |
|
559 |
+ margin: 15px 10px; |
|
560 |
+ .addapproval{ |
|
561 |
+ gap: 5px; |
|
562 |
+ align-items: center; |
|
563 |
+ .form-control, .form-select{margin: 0 ;} |
|
564 |
+ .delete-button{margin-left: 10px;} |
|
565 |
+ } |
|
566 |
+ } |
|
567 |
+ |
|
568 |
+ |
|
569 |
+ |
|
570 |
+ } |
|
571 |
+ .col-12.return{ |
|
572 |
+ .form-label{background-color: #FBC1C1;} |
|
573 |
+ .form-control{background-color: #FFF2F2;} |
|
432 | 574 |
} |
433 | 575 |
.border-x{border: 0;} |
434 |
- .buttons{display: flex; gap: 10px; justify-content: end; |
|
576 |
+ .buttons{display: flex; gap: 10px; justify-content: end; margin-top:5rem; |
|
435 | 577 |
.btn-red{border-color: #E92727; color: #E92727; background-color: #EFF1FA;} |
436 | 578 |
|
579 |
+ } |
|
580 |
+ .hyuga{ |
|
581 |
+ label{line-height: 40rem;} |
|
582 |
+ input.textarea{min-height: 40rem;} |
|
583 |
+ } |
|
584 |
+ .chuljang{ |
|
585 |
+ label{line-height: 17rem;} |
|
586 |
+ input.textarea{min-height: 17rem;} |
|
587 |
+ } |
|
588 |
+ .form-card{ |
|
589 |
+ position: relative; |
|
590 |
+ border: 1px solid #CCCCCC; |
|
591 |
+ border-radius: 10px; |
|
592 |
+ padding: 30px; |
|
593 |
+ h1{margin: 18px 0 48px 0; text-align: center;} |
|
594 |
+ input[readonly] { |
|
595 |
+ border-color: #fff; |
|
596 |
+ background-color: #fff !important; |
|
597 |
+ cursor: context-menu; |
|
598 |
+ } |
|
599 |
+ .hyuga{ |
|
600 |
+ label{line-height: 40rem;} |
|
601 |
+ input.textarea{min-height: 40rem;} |
|
602 |
+ } |
|
603 |
+ .chuljang{ |
|
604 |
+ label{line-height: 17rem;} |
|
605 |
+ input.textarea{min-height: 17rem;} |
|
606 |
+ } |
|
607 |
+ .tbl2{ |
|
608 |
+ width: 30rem; |
|
609 |
+ table{ |
|
610 |
+ .thead{ |
|
611 |
+ .th{ |
|
612 |
+ writing-mode: vertical-rl; padding: 0; width: 50px; |
|
613 |
+ } |
|
614 |
+ } |
|
615 |
+ } |
|
616 |
+ } |
|
617 |
+ .approval-box{position: absolute; right: 30px; top: 30px;} |
|
618 |
+ |
|
619 |
+ |
|
620 |
+ |
|
621 |
+ |
|
622 |
+ } |
|
623 |
+ .tbl2{ |
|
624 |
+ table{ |
|
625 |
+ table-layout: fixed; |
|
626 |
+ margin-bottom: 10px; |
|
627 |
+ .thead{ |
|
628 |
+ .th{background-color: #C7CFE3; } |
|
629 |
+ td{background-color: #EFF1FA; height: 40px; font-weight: 700;} |
|
630 |
+ } |
|
631 |
+ td{padding: 0; border: 1px solid #C7CFE3; } |
|
632 |
+ |
|
633 |
+ } |
|
634 |
+ } |
|
635 |
+ .datepicker-conts{ |
|
636 |
+ margin-bottom: 20px; |
|
637 |
+ .datepicker-input{ |
|
638 |
+ display: flex; |
|
639 |
+ justify-content: end; |
|
640 |
+ align-items: center; |
|
641 |
+ gap: 5px; |
|
642 |
+ .form-control{ |
|
643 |
+ padding-right: 16px; |
|
644 |
+ border-color: #C7CFE3; |
|
645 |
+ } |
|
646 |
+ mark{background-color: transparent;} |
|
647 |
+ } |
|
437 | 648 |
} |
438 | 649 |
|
439 | 650 |
} |
... | ... | @@ -470,4 +681,25 @@ |
470 | 681 |
background: #ccc; |
471 | 682 |
} |
472 | 683 |
|
473 |
- .primary{background-color: #213F9A;}(파일 끝에 줄바꿈 문자 없음) |
|
684 |
+ .primary{background-color: #213F9A;} |
|
685 |
+ a:focus{outline: 0;} |
|
686 |
+ *:focus{outline: 0;} |
|
687 |
+ |
|
688 |
+ .file { |
|
689 |
+ background-color: #EFF1FA; width: 120px; border: #213F9A 1px solid ; border-radius: 10px; padding: 7px 0; |
|
690 |
+ margin-top: 10px; |
|
691 |
+ } |
|
692 |
+ |
|
693 |
+ .file-label { |
|
694 |
+ width: inherit !important; |
|
695 |
+ display: flex; |
|
696 |
+ gap: 5px; |
|
697 |
+ justify-content: center; |
|
698 |
+ line-height: 0 !important; |
|
699 |
+ cursor: pointer; |
|
700 |
+ p{color: #000;} |
|
701 |
+ } |
|
702 |
+ |
|
703 |
+ input[type="file"] { |
|
704 |
+ display: none; |
|
705 |
+ }(파일 끝에 줄바꿈 문자 없음) |
+++ client/views/component/Breadcrumb.vue
... | ... | @@ -0,0 +1,113 @@ |
1 | +<template> | |
2 | + <div v-if="breadcrumbList.length > 0" class="breadcrumb-warp"> | |
3 | + <span class="home"><router-link :to="{path : this.$filters.ctxPath('/adm/main.page')}">홈</router-link> > </span> | |
4 | + <span v-for="(crumb, index) in breadcrumbList" :key="index"> | |
5 | + {{ crumb.menuNm }} | |
6 | + <span v-if="index < breadcrumbList.length - 1"> > </span> | |
7 | + </span> | |
8 | + </div> | |
9 | +</template> | |
10 | + | |
11 | +<script> | |
12 | +export default { | |
13 | + data() { | |
14 | + return { | |
15 | + breadcrumbList: [], | |
16 | + }; | |
17 | + }, | |
18 | + watch: { | |
19 | + // 페이지 이동 시 동작 | |
20 | + $route: { | |
21 | + immediate: true, | |
22 | + handler() { | |
23 | + this.generateBreadcrumb(); | |
24 | + } | |
25 | + }, | |
26 | + // 새로고침 시 동작 | |
27 | + '$store.state.menuList': { | |
28 | + immediate: true, | |
29 | + handler(val) { | |
30 | + if (val && val.length > 0) { | |
31 | + this.generateBreadcrumb(); | |
32 | + } | |
33 | + } | |
34 | + }, | |
35 | + }, | |
36 | + methods: { | |
37 | + generateBreadcrumb() { | |
38 | + const menuState = this.$store.state; | |
39 | + const currentPath = this.$route.path; | |
40 | + | |
41 | + // 관리자: childList 기반 | |
42 | + const findFromTree = (menus, path, trail = []) => { | |
43 | + for (const menu of menus) { | |
44 | + const newTrail = [...trail, menu]; | |
45 | + if (menu.routerUrl === path) { | |
46 | + this.$store.commit('setMenu', menu); | |
47 | + return newTrail; | |
48 | + } | |
49 | + if (menu.childList?.length) { | |
50 | + const found = findFromTree(menu.childList, path, newTrail); | |
51 | + if (found) return found; | |
52 | + } | |
53 | + } | |
54 | + return null; | |
55 | + }; | |
56 | + | |
57 | + // 사용자: upMenuId 기반 | |
58 | + const findFromFlat = (flatMenus, path) => { | |
59 | + const findCurrent = flatMenus.find(menu => menu.routerUrl === path); | |
60 | + if (!findCurrent) return []; | |
61 | + | |
62 | + this.$store.commit('setMenu', findCurrent); | |
63 | + | |
64 | + const buildTrail = (menu, trail = []) => { | |
65 | + const parent = flatMenus.find(m => m.menuId === menu.upMenuId); | |
66 | + if (parent) { | |
67 | + return buildTrail(parent, [parent, ...trail]); | |
68 | + } | |
69 | + return trail; | |
70 | + }; | |
71 | + | |
72 | + const trail = buildTrail(findCurrent); | |
73 | + return [...trail, findCurrent]; | |
74 | + }; | |
75 | + | |
76 | + // 메뉴 리스트 펼치기 | |
77 | + const flattenMenus = (menus) => { | |
78 | + const result = []; | |
79 | + | |
80 | + for (const menu of menus) { | |
81 | + result.push(menu); | |
82 | + if (menu.childList?.length) { | |
83 | + result.push(...flattenMenus(menu.childList)); | |
84 | + } | |
85 | + } | |
86 | + | |
87 | + return result; | |
88 | + } | |
89 | + | |
90 | + let breadcrumb = []; | |
91 | + | |
92 | + // 구조 판단 | |
93 | + if (menuState.menuList?.length) { | |
94 | + // 사용자용 | |
95 | + // const flatMenus = menuState.menuList.flatMap(menu => | |
96 | + // [menu, ...(menu.childList || [])] | |
97 | + // ); | |
98 | + const flatMenus = flattenMenus(menuState.menuList); | |
99 | + breadcrumb = findFromFlat(flatMenus, currentPath); | |
100 | + } else if (menuState.menu?.childList?.length) { | |
101 | + // 관리자용 | |
102 | + breadcrumb = findFromTree(menuState.menu.childList, currentPath); | |
103 | + } | |
104 | + | |
105 | + this.breadcrumbList = breadcrumb; | |
106 | + }, | |
107 | + | |
108 | + }, | |
109 | + mounted(){ | |
110 | + // this.generateBreadcrumb(); | |
111 | + } | |
112 | +}; | |
113 | +</script>(파일 끝에 줄바꿈 문자 없음) |
--- client/views/pages/AppRouter.js
+++ client/views/pages/AppRouter.js
... | ... | @@ -19,8 +19,20 @@ |
19 | 19 |
import approvalList from '../pages/Manager/approval/approvalList.vue'; |
20 | 20 |
import approvalRequest from '../pages/Manager/approval/approvalRequest.vue'; |
21 | 21 |
import HyugaInsert from '../pages/Manager/approval/HyugaInsert.vue'; |
22 |
+import HyugaDetail from '../pages/Manager/approval/HyugaDetail.vue'; |
|
22 | 23 |
import ChuljangInsert from '../pages/Manager/approval/ChuljangInsert.vue'; |
24 |
+import ChuljangDetail from '../pages/Manager/approval/ChuljangDetail.vue'; |
|
25 |
+import ChuljangPumui from '../pages/Manager/approval/ChuljangPumui.vue'; |
|
26 |
+import ChuljangPumuiDetail from '../pages/Manager/attendance/ChuljangPumuiDetail.vue'; |
|
27 |
+import ChuljangBokmyeong from '../pages/Manager/approval/ChuljangBokmyeong.vue'; |
|
28 |
+import ChuljangBokmyeongDetail from '../pages/Manager/attendance/ChuljangBokmyeongDetail.vue'; |
|
29 |
+import ChuljangDetailAll from '../pages/Manager/attendance/ChuljangDetailAll.vue'; |
|
23 | 30 |
import attendance from '../pages/Manager/attendance/attendance.vue'; |
31 |
+import myAttendance from '../pages/Manager/attendance/myAttendance.vue'; |
|
32 |
+import buseoAttendance from '../pages/Manager/attendance/buseoAttendance.vue'; |
|
33 |
+import AttendanceDetail from '../pages/Manager/attendance/AttendanceDetail.vue'; |
|
34 |
+import hyugaStatue from '../pages/Manager/attendance/hyugaStatue.vue'; |
|
35 |
+import ChuljangStatue from '../pages/Manager/attendance/ChuljangStatue.vue'; |
|
24 | 36 |
import task from '../pages/Manager/task/task.vue'; |
25 | 37 |
import financial from '../pages/Manager/financial/financial.vue'; |
26 | 38 |
import asset from '../pages/Manager/asset/asset.vue'; |
... | ... | @@ -54,11 +66,29 @@ |
54 | 66 |
name: 'approvalList', |
55 | 67 |
component: approvalList |
56 | 68 |
}, |
57 |
- { path: '/ChuljangInsert.page', name: 'ChuljangInsert', component: ChuljangInsert }, |
|
69 |
+ { path: '/ChuljangPumui.page', name: 'ChuljangPumui', component: ChuljangPumui }, |
|
70 |
+ { path: '/ChuljangBokmyeong.page', name: 'ChuljangBokmyeong', component: ChuljangBokmyeong }, |
|
71 |
+ |
|
72 |
+ |
|
73 |
+ ] |
|
74 |
+ }, //결재관리 |
|
75 |
+ { path: '/attendance-management.page', name: 'attendance', component: attendance, |
|
76 |
+ children: [ |
|
77 |
+ { path: '/myAttendance.page', name: 'myAttendance', component: myAttendance }, |
|
78 |
+ { path: '/buseoAttendance.page', name: 'buseoAttendance', component: buseoAttendance }, |
|
79 |
+ { path: '/AttendanceDetail.page', name: 'AttendanceDetail', component: AttendanceDetail }, |
|
80 |
+ { path: '/hyugaStatue.page', name: 'hyugaStatue', component: hyugaStatue }, |
|
81 |
+ { path: '/HyugaDetail.page', name: 'HyugaDetail', component: HyugaDetail }, |
|
58 | 82 |
{ path: '/HyugaInsert.page', name: 'HyugaInsert', component: HyugaInsert }, |
59 |
- ] |
|
60 |
- }, //결재관리 |
|
61 |
- { path: '/attendance-management.page', name: 'attendance', component: attendance }, //근태관리 |
|
83 |
+ { path: '/ChuljangStatue.page', name: 'ChuljangStatue', component: ChuljangStatue }, |
|
84 |
+ { path: '/ChuljangDetail.page', name: 'ChuljangDetail', component: ChuljangDetail }, |
|
85 |
+ { path: '/ChuljangInsert.page', name: 'ChuljangInsert', component: ChuljangInsert }, |
|
86 |
+ { path: '/ChuljangPumuiDetail.page', name: 'ChuljangPumuiDetail', component: ChuljangPumuiDetail }, |
|
87 |
+ { path: '/ChuljangBokmyeongDetail.page', name: 'ChuljangBokmyeongDetail', component: ChuljangBokmyeongDetail }, |
|
88 |
+ { path: '/ChuljangDetailAll.page', name: 'ChuljangDetailAll', component: ChuljangDetailAll }, |
|
89 |
+ |
|
90 |
+ ] |
|
91 |
+ }, //근태관리 |
|
62 | 92 |
{ path: '/task-management.page', name: 'task', component: task }, //업무관리 |
63 | 93 |
{ path: '/financial-management.page', name: 'financial', component: financial }, //재무관리 |
64 | 94 |
{ path: '/asset-management.page', name: 'asset', component: asset }, //자산관리 |
+++ client/views/pages/Manager/approval/ChuljangBokmyeong.vue
... | ... | @@ -0,0 +1,201 @@ |
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 " :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" id="youremail" 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" id="yourPassword" 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" id="youremail" 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" id="yourPassword" 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" id="yourName" readonly> | |
50 | + </div> | |
51 | + <div class="col-12"> | |
52 | + <label for="yourName" class="form-label">출장목적</label> | |
53 | + <input v-model="name" type="text" name="name" class="form-control " id="yourName" 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 " id="yourName" readonly> | |
58 | + </div> | |
59 | + <div class="col-12 chuljang"> | |
60 | + <label for="yourName" class="form-label">복명내용</label> | |
61 | + <input v-model="name" type="text" name="name" class="form-control textarea " id="yourName" readonly> | |
62 | + </div> | |
63 | + <div class="col-12"> | |
64 | + <label for="yourName" class="form-label">법인카드</label> | |
65 | + <input v-model="name" type="text" name="name" class="form-control " id="yourName" readonly> | |
66 | + </div> | |
67 | + <div class="col-12"> | |
68 | + <label for="yourName" class="form-label">법인차량</label> | |
69 | + <input v-model="name" type="text" name="name" class="form-control " id="yourName" readonly> | |
70 | + </div> | |
71 | + <div class="col-12"> | |
72 | + <label for="yourName" class="form-label">여비계산</label> | |
73 | + <input v-model="name" type="text" name="name" class="form-control " id="yourName" readonly> | |
74 | + </div> | |
75 | + <div class="col-12 border-x"> | |
76 | + <label for="yourName" class="form-label">복명 신청일</label> | |
77 | + <input v-model="name" type="text" name="name" class="form-control" id="yourName" readonly placeholder="2025-01-01"> | |
78 | + </div> | |
79 | + | |
80 | + | |
81 | + </form> | |
82 | + </div> | |
83 | + <div class="buttons"> | |
84 | + <button class="btn primary" type="submit">승인</button> | |
85 | + <button class="btn btn-red " type="submit">반려</button> | |
86 | + <button class="btn tertiary " type="submit">목록</button> | |
87 | + </div> | |
88 | + | |
89 | + </div> | |
90 | + </div> | |
91 | +</template> | |
92 | + | |
93 | +<script> | |
94 | +export default { | |
95 | + data() { | |
96 | + const today = new Date().toISOString().split('T')[0]; | |
97 | + return { | |
98 | + startDate: today, | |
99 | + startTime: "09:00", // 기본 시작 시간 09:00 | |
100 | + endDate: today, | |
101 | + endTime: "18:00", // 기본 종료 시간 18:00 | |
102 | + category: "", | |
103 | + dayCount: 1, | |
104 | + reason: "", // 사유 | |
105 | + listData: [ | |
106 | + { | |
107 | + type: '연차', | |
108 | + approvalType: '결재', | |
109 | + applicant: '홍길동', | |
110 | + period: '2025-05-10 ~ 2025-15-03', | |
111 | + requestDate: '2025-04-25', | |
112 | + status: '대기' | |
113 | + }, { | |
114 | + type: '반차', | |
115 | + approvalType: '전결', | |
116 | + applicant: '홍길동', | |
117 | + period: '2025-05-01 ~ 2025-05-03', | |
118 | + requestDate: '2025-04-25', | |
119 | + status: '승인' | |
120 | + }], | |
121 | + }; | |
122 | + }, | |
123 | + computed: { | |
124 | + // Pinia Store의 상태를 가져옵니다. | |
125 | + loginUser() { | |
126 | + const authStore = useAuthStore(); | |
127 | + return authStore.getLoginUser; | |
128 | + }, | |
129 | + }, | |
130 | + methods: { | |
131 | + // 폼 검증 메서드 | |
132 | + validateForm() { | |
133 | + // 필수 입력 필드 체크 | |
134 | + if ( | |
135 | + this.category && | |
136 | + this.startDate && | |
137 | + this.startTime && | |
138 | + this.endDate && | |
139 | + this.endTime && | |
140 | + this.dayCount > 0 && | |
141 | + this.reason.trim() !== "" | |
142 | + ) { | |
143 | + this.isFormValid = true; | |
144 | + } else { | |
145 | + this.isFormValid = false; | |
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 | + this.validateForm(); // dayCount 변경 후 폼 재검증 | |
174 | + }, | |
175 | + handleSubmit() { | |
176 | + this.validateForm(); // 제출 시 유효성 확인 | |
177 | + if (this.isFormValid) { | |
178 | + localStorage.setItem('HyugaFormData', JSON.stringify(this.$data)); | |
179 | + alert("승인 요청이 완료되었습니다."); | |
180 | + // 추가 처리 로직 (API 요청 등) | |
181 | + } else { | |
182 | + alert("모든 필드를 올바르게 작성해주세요."); | |
183 | + } | |
184 | + }, | |
185 | + | |
186 | + | |
187 | + }, | |
188 | + mounted() { | |
189 | + // Load the saved form data when the page is loaded | |
190 | + this.loadFormData(); | |
191 | + }, | |
192 | + watch: { | |
193 | + startDate: 'calculateDayCount', | |
194 | + startTime: 'calculateDayCount', | |
195 | + endDate: 'calculateDayCount', | |
196 | + endTime: 'calculateDayCount', | |
197 | + reason: "validateForm", | |
198 | + category: 'category', | |
199 | + }, | |
200 | +}; | |
201 | +</script> |
+++ client/views/pages/Manager/approval/ChuljangDetail.vue
... | ... | @@ -0,0 +1,186 @@ |
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 " :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" id="youremail" 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" id="yourPassword" 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" id="youremail" 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" id="yourPassword" 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" id="yourName" 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" id="yourName" 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 " id="yourName" readonly> | |
58 | + </div> | |
59 | + <div class="col-12 border-x" :class="{ return: isReturned }"> | |
60 | + <label for="yourName" class="form-label ">반려사유</label> | |
61 | + <input v-model="name" type="text" name="name" class="form-control" id="yourName" readonly > | |
62 | + </div> | |
63 | + | |
64 | + | |
65 | + </form> | |
66 | + </div> | |
67 | + <div class="buttons"> | |
68 | + <button class="btn btn-red" type="submit">신청취소</button> | |
69 | + <button class="btn secondary" type="submit"> {{ isReturned ? '재신청' : '수정' }}</button> | |
70 | + <button class="btn tertiary " type="submit">목록</button> | |
71 | + </div> | |
72 | + | |
73 | + </div> | |
74 | + </div> | |
75 | +</template> | |
76 | + | |
77 | +<script> | |
78 | +export default { | |
79 | + data() { | |
80 | + const today = new Date().toISOString().split('T')[0]; | |
81 | + return { | |
82 | + isReturned: true, | |
83 | + startDate: today, | |
84 | + startTime: "09:00", // 기본 시작 시간 09:00 | |
85 | + endDate: today, | |
86 | + endTime: "18:00", // 기본 종료 시간 18:00 | |
87 | + category: "", | |
88 | + dayCount: 1, | |
89 | + reason: "", // 사유 | |
90 | + listData: [ | |
91 | + { | |
92 | + type: '연차', | |
93 | + approvalType: '결재', | |
94 | + applicant: '홍길동', | |
95 | + period: '2025-05-10 ~ 2025-15-03', | |
96 | + requestDate: '2025-04-25', | |
97 | + status: '대기' | |
98 | + }, { | |
99 | + type: '반차', | |
100 | + approvalType: '전결', | |
101 | + applicant: '홍길동', | |
102 | + period: '2025-05-01 ~ 2025-05-03', | |
103 | + requestDate: '2025-04-25', | |
104 | + status: '승인' | |
105 | + }], | |
106 | + }; | |
107 | + }, | |
108 | + computed: { | |
109 | + // Pinia Store의 상태를 가져옵니다. | |
110 | + loginUser() { | |
111 | + const authStore = useAuthStore(); | |
112 | + return authStore.getLoginUser; | |
113 | + }, | |
114 | + }, | |
115 | + methods: { | |
116 | + // 폼 검증 메서드 | |
117 | + validateForm() { | |
118 | + // 필수 입력 필드 체크 | |
119 | + if ( | |
120 | + this.category && | |
121 | + this.startDate && | |
122 | + this.startTime && | |
123 | + this.endDate && | |
124 | + this.endTime && | |
125 | + this.dayCount > 0 && | |
126 | + this.reason.trim() !== "" | |
127 | + ) { | |
128 | + this.isFormValid = true; | |
129 | + } else { | |
130 | + this.isFormValid = false; | |
131 | + } | |
132 | + }, | |
133 | + calculateDayCount() { | |
134 | + const start = new Date(`${this.startDate}T${this.startTime}:00`); | |
135 | + const end = new Date(`${this.endDate}T${this.endTime}:00`); | |
136 | + | |
137 | + let totalHours = (end - start) / (1000 * 60 * 60); // 밀리초를 시간 단위로 변환 | |
138 | + | |
139 | + if (this.startDate !== this.endDate) { | |
140 | + // 시작일과 종료일이 다른경우 | |
141 | + const startDateObj = new Date(this.startDate); | |
142 | + const endDateObj = new Date(this.endDate); | |
143 | + const daysDifference = (endDateObj - startDateObj) / (1000 * 60 * 60 * 24); // 두 날짜 사이의 차이를 일수로 계산 | |
144 | + if (this.startTime !== "09:00" || this.endTime !== "18:00") { | |
145 | + this.dayCount = daysDifference + 0.5; // 시간 조건이 기준에서 벗어날 경우 | |
146 | + } else { | |
147 | + this.dayCount = Math.ceil(daysDifference + 1); // 시간 조건이 기준에 맞을 경우 | |
148 | + } | |
149 | + } else { | |
150 | + // 시작일과 종료일이 같은 경우 | |
151 | + if (this.startTime !== "09:00" || this.endTime !== "18:00") { | |
152 | + this.dayCount = 0.5; // 시작 시간 또는 종료 시간이 기준과 다를 경우 0.5 | |
153 | + } else { | |
154 | + this.dayCount = 1; // 기준 시간(09:00~18:00)이 맞으면 1일로 간주 | |
155 | + } | |
156 | + } | |
157 | + | |
158 | + this.validateForm(); // dayCount 변경 후 폼 재검증 | |
159 | + }, | |
160 | + handleSubmit() { | |
161 | + this.validateForm(); // 제출 시 유효성 확인 | |
162 | + if (this.isFormValid) { | |
163 | + localStorage.setItem('HyugaFormData', JSON.stringify(this.$data)); | |
164 | + alert("승인 요청이 완료되었습니다."); | |
165 | + // 추가 처리 로직 (API 요청 등) | |
166 | + } else { | |
167 | + alert("모든 필드를 올바르게 작성해주세요."); | |
168 | + } | |
169 | + }, | |
170 | + | |
171 | + | |
172 | + }, | |
173 | + mounted() { | |
174 | + // Load the saved form data when the page is loaded | |
175 | + this.loadFormData(); | |
176 | + }, | |
177 | + watch: { | |
178 | + startDate: 'calculateDayCount', | |
179 | + startTime: 'calculateDayCount', | |
180 | + endDate: 'calculateDayCount', | |
181 | + endTime: 'calculateDayCount', | |
182 | + reason: "validateForm", | |
183 | + category: 'category', | |
184 | + }, | |
185 | +}; | |
186 | +</script> |
--- client/views/pages/Manager/approval/ChuljangInsert.vue
+++ client/views/pages/Manager/approval/ChuljangInsert.vue
... | ... | @@ -1,106 +1,155 @@ |
1 | 1 |
<template> |
2 |
- <div class="pagetitle"> |
|
3 |
- <h2>출장신청</h2> |
|
4 |
- </div><!-- End Page Title --> |
|
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" @submit.prevent="handleSubmit"> |
|
5 | 7 |
|
6 |
- <section class="section"> |
|
7 |
- <div class="card"> |
|
8 |
- <div class="card-body"> |
|
9 | 8 |
|
10 |
- <!-- Multi Columns Form --> |
|
11 |
- <form class="row g-3 pt-3" @submit.prevent="handleSubmit"> |
|
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" /> |
|
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" /> |
|
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" /> |
|
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" /> |
|
24 |
+ </div> |
|
12 | 25 |
|
13 |
- <div class="col-md-5"> |
|
14 |
- <label for="startDate" class="form-label">시작일</label> |
|
15 |
- <div class="d-flex gap-1"> |
|
16 |
- <input type="date" class="form-control" id="startDate" v-model="startDate" /> |
|
17 |
- <select class="form-control" id="startTime" v-model="startTime"> |
|
18 |
- <option value="09:00">09:00</option> |
|
19 |
- <option value="10:00">10:00</option> |
|
20 |
- <option value="11:00">11:00</option> |
|
21 |
- <option value="12:00">12:00</option> |
|
22 |
- <option value="13:00">13:00</option> |
|
23 |
- <option value="14:00">14:00</option> |
|
24 |
- <option value="15:00">15:00</option> |
|
25 |
- <option value="16:00">16:00</option> |
|
26 |
- <option value="17:00">17:00</option> |
|
27 |
- <option value="18:00">18:00</option> |
|
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" /> |
|
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" /> |
|
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" /> |
|
37 |
+ </div> |
|
38 |
+ |
|
39 |
+ <div class="col-12"> |
|
40 |
+ <label for="member" class="form-label"> |
|
41 |
+ 승인자 |
|
42 |
+ <button type="button" title="추가" @click="addApproval"> |
|
43 |
+ <PlusCircleFilled /> |
|
44 |
+ </button> |
|
45 |
+ </label> |
|
46 |
+ |
|
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> |
|
28 | 53 |
</select> |
29 |
- </div> |
|
30 |
- </div> |
|
31 | 54 |
|
32 |
- <div class="col-md-5"> |
|
33 |
- <label for="endDate" class="form-label">종료일</label> |
|
34 |
- <div class="d-flex gap-1"> |
|
35 |
- <input type="date" class="form-control" id="endDate" v-model="endDate" /> |
|
36 |
- <select class="form-control" id="endTime" v-model="endTime"> |
|
37 |
- <option value="09:00">09:00</option> |
|
38 |
- <option value="10:00">10:00</option> |
|
39 |
- <option value="11:00">11:00</option> |
|
40 |
- <option value="12:00">12:00</option> |
|
41 |
- <option value="13:00">13:00</option> |
|
42 |
- <option value="14:00">14:00</option> |
|
43 |
- <option value="15:00">15:00</option> |
|
44 |
- <option value="16:00">16:00</option> |
|
45 |
- <option value="17:00">17:00</option> |
|
46 |
- <option value="18:00">18:00</option> |
|
47 |
- </select> |
|
48 |
- </div> |
|
49 |
- </div> |
|
50 |
- |
|
51 |
- <div class="col-12"> |
|
52 |
- <label for="where" class="form-label">출장지</label> |
|
53 |
- <input type="text" class="form-control" id="where" v-model="where" /> |
|
54 |
- </div> |
|
55 |
- |
|
56 |
- <div class="col-12"> |
|
57 |
- <label for="purpose" class="form-label">출장목적</label> |
|
58 |
- <input type="text" class="form-control" id="purpose" v-model="purpose" /> |
|
59 |
- </div> |
|
60 |
- |
|
61 |
- <div class="col-6"> |
|
62 |
- <label for="member" class="form-label">동행자</label> |
|
63 |
- <div class="search-bar d-flex gap-2"> |
|
64 |
- <form class="search-form d-flex align-items-center" method="POST" action="#" |
|
65 |
- @submit.prevent="updateMember"> |
|
66 |
- <input type="text" v-model="searchQuery" name="query" placeholder="Search" title="Enter search keyword" /> |
|
67 |
- <button type="submit" title="Search"><PlusOutlined /></button> |
|
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> |
|
68 | 60 |
</form> |
69 |
- <input type="text" class="select-member" :value="selectedMember.join(' ')" readonly /> |
|
70 | 61 |
</div> |
71 | 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="addApproval"> |
|
72 |
+ <PlusCircleFilled /> |
|
73 |
+ </button> |
|
74 |
+ </label> |
|
72 | 75 |
|
73 |
- <div class="text-end"> |
|
74 |
- <button type="submit" class="btn primary">승인요청</button> |
|
75 |
- <button type="reset" class="btn secondary">취소</button> |
|
76 |
+ <div class="approval-container"> |
|
77 |
+ <div v-for="(approval, index) in approvals" :key="index" class="d-flex gap-2 addapproval mb-2"> |
|
78 |
+ <select class="form-select" v-model="approval.category" style="width: 140px;"> |
|
79 |
+ <option value="결재">개인결제</option> |
|
80 |
+ <option value="전결">법인결제</option> |
|
81 |
+ </select> |
|
82 |
+ <select class="form-select" v-model="approval.category" style="width: 110px;"> |
|
83 |
+ <option value="결재">법인</option> |
|
84 |
+ <option value="전결">개인</option> |
|
85 |
+ </select> |
|
86 |
+ <select class="form-select" v-model="approval.category" style="width: 110px;"> |
|
87 |
+ <option value="결재" selected>구분</option> |
|
88 |
+ <option value="전결">개인</option> |
|
89 |
+ </select> |
|
90 |
+ |
|
91 |
+ <input type="text" class="form-control" v-model="approval.name" style="max-width: 150px;" /> |
|
92 |
+ <div class=""> |
|
93 |
+ <!-- 커스텀 업로드 버튼 --> |
|
94 |
+ <button> |
|
95 |
+ <label for="fileUpload" > |
|
96 |
+ 영수증 첨부 |
|
97 |
+ </label> |
|
98 |
+ </button> |
|
99 |
+ |
|
100 |
+ <!-- 실제 파일 input (숨김 처리) --> |
|
101 |
+ <input |
|
102 |
+ id="fileUpload" |
|
103 |
+ type="file" |
|
104 |
+ @change="handleFileUpload" |
|
105 |
+ class="hidden-file-input" |
|
106 |
+ /> |
|
107 |
+ |
|
108 |
+ |
|
109 |
+ </div> |
|
110 |
+ <!-- 선택된 파일 이름 표시 --> |
|
111 |
+ <span v-if="fileName" class="file-name">{{ fileName }}</span> |
|
112 |
+ <button type="button" @click="removeApproval(index)" class="delete-button"> |
|
113 |
+ <CloseOutlined /> |
|
114 |
+ </button> |
|
115 |
+ </div> |
|
76 | 116 |
</div> |
117 |
+ </div> |
|
77 | 118 |
|
78 |
- </form><!-- End Multi Columns Form --> |
|
79 |
- |
|
119 |
+ </form> |
|
120 |
+ <div class="buttons"> |
|
121 |
+ <button type="submit" class="btn primary">승인요청</button> |
|
122 |
+ <button type="reset" class="btn secondary">취소</button> |
|
80 | 123 |
</div> |
124 |
+ |
|
81 | 125 |
</div> |
82 |
- </section> |
|
126 |
+ </div> |
|
83 | 127 |
</template> |
84 | 128 |
|
85 | 129 |
<script> |
86 |
-import { PlusOutlined } from '@ant-design/icons-vue'; |
|
130 |
+import { PlusCircleFilled, CloseOutlined } from '@ant-design/icons-vue'; |
|
87 | 131 |
|
88 | 132 |
export default { |
89 | 133 |
data() { |
90 | 134 |
const today = new Date().toISOString().split('T')[0]; |
91 | 135 |
return { |
136 |
+ fileName: '', |
|
92 | 137 |
startDate: today, |
93 | 138 |
startTime: '09:00', |
94 | 139 |
endDate: today, |
95 | 140 |
endTime: '18:00', |
96 | 141 |
where: '', |
97 | 142 |
purpose: '', |
98 |
- selectedMember: [], |
|
99 |
- searchQuery: '', |
|
143 |
+ approvals: [ |
|
144 |
+ { |
|
145 |
+ category: '결재', |
|
146 |
+ name: '', |
|
147 |
+ }, |
|
148 |
+ ], |
|
100 | 149 |
}; |
101 | 150 |
}, |
102 | 151 |
components: { |
103 |
- PlusOutlined, |
|
152 |
+ PlusCircleFilled, CloseOutlined |
|
104 | 153 |
}, |
105 | 154 |
computed: { |
106 | 155 |
loginUser() { |
... | ... | @@ -110,13 +159,21 @@ |
110 | 159 |
}, |
111 | 160 |
|
112 | 161 |
methods: { |
113 |
- updateMember() { |
|
114 |
- // Add the search query to the selectedMembers array if it's not empty |
|
115 |
- if (this.searchQuery.trim()) { |
|
116 |
- this.selectedMember.push(this.searchQuery.trim()); |
|
162 |
+ handleFileUpload(event) { |
|
163 |
+ const file = event.target.files[0]; |
|
164 |
+ if (file) { |
|
165 |
+ this.fileName = file.name; |
|
117 | 166 |
} |
118 |
- // Clear the search query after adding it to selectedMembers |
|
119 |
- this.searchQuery = ''; |
|
167 |
+ }, |
|
168 |
+ addApproval() { |
|
169 |
+ this.approvals.push({ |
|
170 |
+ category: '결재', |
|
171 |
+ name: '', |
|
172 |
+ }); |
|
173 |
+ }, |
|
174 |
+ // 승인자 삭제 |
|
175 |
+ removeApproval(index) { |
|
176 |
+ this.approvals.splice(index, 1); |
|
120 | 177 |
}, |
121 | 178 |
validateForm() { |
122 | 179 |
// 필수 입력 필드 체크 |
... | ... | @@ -186,8 +243,8 @@ |
186 | 243 |
startTime: 'calculateDayCount', |
187 | 244 |
endDate: 'calculateDayCount', |
188 | 245 |
endTime: 'calculateDayCount', |
189 |
- where: 'validateForm', |
|
190 |
- purpose: "validateForm", |
|
246 |
+ where: 'validateForm', |
|
247 |
+ purpose: "validateForm", |
|
191 | 248 |
}, |
192 | 249 |
}; |
193 | 250 |
</script> |
+++ client/views/pages/Manager/approval/ChuljangPumui.vue
... | ... | @@ -0,0 +1,197 @@ |
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 " :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" id="youremail" 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" id="yourPassword" 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" id="youremail" 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" id="yourPassword" 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" id="yourName" readonly> | |
50 | + </div> | |
51 | + <div class="col-12"> | |
52 | + <label for="yourName" class="form-label">출장목적</label> | |
53 | + <input v-model="name" type="text" name="name" class="form-control " id="yourName" 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 " id="yourName" readonly> | |
58 | + </div> | |
59 | + <div class="col-12 chuljang"> | |
60 | + <label for="yourName" class="form-label">내용</label> | |
61 | + <input v-model="name" type="text" name="name" class="form-control textarea " id="yourName" readonly> | |
62 | + </div> | |
63 | + <div class="col-12"> | |
64 | + <label for="yourName" class="form-label">법인카드</label> | |
65 | + <input v-model="name" type="text" name="name" class="form-control " id="yourName" readonly> | |
66 | + </div> | |
67 | + <div class="col-12"> | |
68 | + <label for="yourName" class="form-label">법인차량</label> | |
69 | + <input v-model="name" type="text" name="name" class="form-control " id="yourName" readonly> | |
70 | + </div> | |
71 | + <div class="col-12 border-x"> | |
72 | + <label for="yourName" class="form-label">품의 신청일</label> | |
73 | + <input v-model="name" type="text" name="name" class="form-control" id="yourName" readonly placeholder="2025-01-01"> | |
74 | + </div> | |
75 | + | |
76 | + | |
77 | + </form> | |
78 | + </div> | |
79 | + <div class="buttons"> | |
80 | + <button class="btn primary" type="submit">승인</button> | |
81 | + <button class="btn btn-red " type="submit">반려</button> | |
82 | + <button class="btn tertiary " type="submit">목록</button> | |
83 | + </div> | |
84 | + | |
85 | + </div> | |
86 | + </div> | |
87 | +</template> | |
88 | + | |
89 | +<script> | |
90 | +export default { | |
91 | + data() { | |
92 | + const today = new Date().toISOString().split('T')[0]; | |
93 | + return { | |
94 | + startDate: today, | |
95 | + startTime: "09:00", // 기본 시작 시간 09:00 | |
96 | + endDate: today, | |
97 | + endTime: "18:00", // 기본 종료 시간 18:00 | |
98 | + category: "", | |
99 | + dayCount: 1, | |
100 | + reason: "", // 사유 | |
101 | + listData: [ | |
102 | + { | |
103 | + type: '연차', | |
104 | + approvalType: '결재', | |
105 | + applicant: '홍길동', | |
106 | + period: '2025-05-10 ~ 2025-15-03', | |
107 | + requestDate: '2025-04-25', | |
108 | + status: '대기' | |
109 | + }, { | |
110 | + type: '반차', | |
111 | + approvalType: '전결', | |
112 | + applicant: '홍길동', | |
113 | + period: '2025-05-01 ~ 2025-05-03', | |
114 | + requestDate: '2025-04-25', | |
115 | + status: '승인' | |
116 | + }], | |
117 | + }; | |
118 | + }, | |
119 | + computed: { | |
120 | + // Pinia Store의 상태를 가져옵니다. | |
121 | + loginUser() { | |
122 | + const authStore = useAuthStore(); | |
123 | + return authStore.getLoginUser; | |
124 | + }, | |
125 | + }, | |
126 | + methods: { | |
127 | + // 폼 검증 메서드 | |
128 | + validateForm() { | |
129 | + // 필수 입력 필드 체크 | |
130 | + if ( | |
131 | + this.category && | |
132 | + this.startDate && | |
133 | + this.startTime && | |
134 | + this.endDate && | |
135 | + this.endTime && | |
136 | + this.dayCount > 0 && | |
137 | + this.reason.trim() !== "" | |
138 | + ) { | |
139 | + this.isFormValid = true; | |
140 | + } else { | |
141 | + this.isFormValid = false; | |
142 | + } | |
143 | + }, | |
144 | + calculateDayCount() { | |
145 | + const start = new Date(`${this.startDate}T${this.startTime}:00`); | |
146 | + const end = new Date(`${this.endDate}T${this.endTime}:00`); | |
147 | + | |
148 | + let totalHours = (end - start) / (1000 * 60 * 60); // 밀리초를 시간 단위로 변환 | |
149 | + | |
150 | + if (this.startDate !== this.endDate) { | |
151 | + // 시작일과 종료일이 다른경우 | |
152 | + const startDateObj = new Date(this.startDate); | |
153 | + const endDateObj = new Date(this.endDate); | |
154 | + const daysDifference = (endDateObj - startDateObj) / (1000 * 60 * 60 * 24); // 두 날짜 사이의 차이를 일수로 계산 | |
155 | + if (this.startTime !== "09:00" || this.endTime !== "18:00") { | |
156 | + this.dayCount = daysDifference + 0.5; // 시간 조건이 기준에서 벗어날 경우 | |
157 | + } else { | |
158 | + this.dayCount = Math.ceil(daysDifference + 1); // 시간 조건이 기준에 맞을 경우 | |
159 | + } | |
160 | + } else { | |
161 | + // 시작일과 종료일이 같은 경우 | |
162 | + if (this.startTime !== "09:00" || this.endTime !== "18:00") { | |
163 | + this.dayCount = 0.5; // 시작 시간 또는 종료 시간이 기준과 다를 경우 0.5 | |
164 | + } else { | |
165 | + this.dayCount = 1; // 기준 시간(09:00~18:00)이 맞으면 1일로 간주 | |
166 | + } | |
167 | + } | |
168 | + | |
169 | + this.validateForm(); // dayCount 변경 후 폼 재검증 | |
170 | + }, | |
171 | + handleSubmit() { | |
172 | + this.validateForm(); // 제출 시 유효성 확인 | |
173 | + if (this.isFormValid) { | |
174 | + localStorage.setItem('HyugaFormData', JSON.stringify(this.$data)); | |
175 | + alert("승인 요청이 완료되었습니다."); | |
176 | + // 추가 처리 로직 (API 요청 등) | |
177 | + } else { | |
178 | + alert("모든 필드를 올바르게 작성해주세요."); | |
179 | + } | |
180 | + }, | |
181 | + | |
182 | + | |
183 | + }, | |
184 | + mounted() { | |
185 | + // Load the saved form data when the page is loaded | |
186 | + this.loadFormData(); | |
187 | + }, | |
188 | + watch: { | |
189 | + startDate: 'calculateDayCount', | |
190 | + startTime: 'calculateDayCount', | |
191 | + endDate: 'calculateDayCount', | |
192 | + endTime: 'calculateDayCount', | |
193 | + reason: "validateForm", | |
194 | + category: 'category', | |
195 | + }, | |
196 | +}; | |
197 | +</script> |
+++ client/views/pages/Manager/approval/HyugaDetail.vue
... | ... | @@ -0,0 +1,186 @@ |
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 " :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" id="youremail" 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" id="yourPassword" 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" id="youremail" 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" id="yourPassword" 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" id="yourName" 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" id="yourName" 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 " id="yourName" readonly> | |
58 | + </div> | |
59 | + <div class="col-12 border-x" :class="{ return: isReturned }"> | |
60 | + <label for="yourName" class="form-label ">반려사유</label> | |
61 | + <input v-model="name" type="text" name="name" class="form-control" id="yourName" readonly > | |
62 | + </div> | |
63 | + | |
64 | + | |
65 | + </form> | |
66 | + </div> | |
67 | + <div class="buttons"> | |
68 | + <button class="btn btn-red" type="submit">신청취소</button> | |
69 | + <button class="btn secondary" type="submit"> {{ isReturned ? '재신청' : '수정' }}</button> | |
70 | + <button class="btn tertiary " type="submit">목록</button> | |
71 | + </div> | |
72 | + | |
73 | + </div> | |
74 | + </div> | |
75 | +</template> | |
76 | + | |
77 | +<script> | |
78 | +export default { | |
79 | + data() { | |
80 | + const today = new Date().toISOString().split('T')[0]; | |
81 | + return { | |
82 | + isReturned: true, | |
83 | + startDate: today, | |
84 | + startTime: "09:00", // 기본 시작 시간 09:00 | |
85 | + endDate: today, | |
86 | + endTime: "18:00", // 기본 종료 시간 18:00 | |
87 | + category: "", | |
88 | + dayCount: 1, | |
89 | + reason: "", // 사유 | |
90 | + listData: [ | |
91 | + { | |
92 | + type: '연차', | |
93 | + approvalType: '결재', | |
94 | + applicant: '홍길동', | |
95 | + period: '2025-05-10 ~ 2025-15-03', | |
96 | + requestDate: '2025-04-25', | |
97 | + status: '대기' | |
98 | + }, { | |
99 | + type: '반차', | |
100 | + approvalType: '전결', | |
101 | + applicant: '홍길동', | |
102 | + period: '2025-05-01 ~ 2025-05-03', | |
103 | + requestDate: '2025-04-25', | |
104 | + status: '승인' | |
105 | + }], | |
106 | + }; | |
107 | + }, | |
108 | + computed: { | |
109 | + // Pinia Store의 상태를 가져옵니다. | |
110 | + loginUser() { | |
111 | + const authStore = useAuthStore(); | |
112 | + return authStore.getLoginUser; | |
113 | + }, | |
114 | + }, | |
115 | + methods: { | |
116 | + // 폼 검증 메서드 | |
117 | + validateForm() { | |
118 | + // 필수 입력 필드 체크 | |
119 | + if ( | |
120 | + this.category && | |
121 | + this.startDate && | |
122 | + this.startTime && | |
123 | + this.endDate && | |
124 | + this.endTime && | |
125 | + this.dayCount > 0 && | |
126 | + this.reason.trim() !== "" | |
127 | + ) { | |
128 | + this.isFormValid = true; | |
129 | + } else { | |
130 | + this.isFormValid = false; | |
131 | + } | |
132 | + }, | |
133 | + calculateDayCount() { | |
134 | + const start = new Date(`${this.startDate}T${this.startTime}:00`); | |
135 | + const end = new Date(`${this.endDate}T${this.endTime}:00`); | |
136 | + | |
137 | + let totalHours = (end - start) / (1000 * 60 * 60); // 밀리초를 시간 단위로 변환 | |
138 | + | |
139 | + if (this.startDate !== this.endDate) { | |
140 | + // 시작일과 종료일이 다른경우 | |
141 | + const startDateObj = new Date(this.startDate); | |
142 | + const endDateObj = new Date(this.endDate); | |
143 | + const daysDifference = (endDateObj - startDateObj) / (1000 * 60 * 60 * 24); // 두 날짜 사이의 차이를 일수로 계산 | |
144 | + if (this.startTime !== "09:00" || this.endTime !== "18:00") { | |
145 | + this.dayCount = daysDifference + 0.5; // 시간 조건이 기준에서 벗어날 경우 | |
146 | + } else { | |
147 | + this.dayCount = Math.ceil(daysDifference + 1); // 시간 조건이 기준에 맞을 경우 | |
148 | + } | |
149 | + } else { | |
150 | + // 시작일과 종료일이 같은 경우 | |
151 | + if (this.startTime !== "09:00" || this.endTime !== "18:00") { | |
152 | + this.dayCount = 0.5; // 시작 시간 또는 종료 시간이 기준과 다를 경우 0.5 | |
153 | + } else { | |
154 | + this.dayCount = 1; // 기준 시간(09:00~18:00)이 맞으면 1일로 간주 | |
155 | + } | |
156 | + } | |
157 | + | |
158 | + this.validateForm(); // dayCount 변경 후 폼 재검증 | |
159 | + }, | |
160 | + handleSubmit() { | |
161 | + this.validateForm(); // 제출 시 유효성 확인 | |
162 | + if (this.isFormValid) { | |
163 | + localStorage.setItem('HyugaFormData', JSON.stringify(this.$data)); | |
164 | + alert("승인 요청이 완료되었습니다."); | |
165 | + // 추가 처리 로직 (API 요청 등) | |
166 | + } else { | |
167 | + alert("모든 필드를 올바르게 작성해주세요."); | |
168 | + } | |
169 | + }, | |
170 | + | |
171 | + | |
172 | + }, | |
173 | + mounted() { | |
174 | + // Load the saved form data when the page is loaded | |
175 | + this.loadFormData(); | |
176 | + }, | |
177 | + watch: { | |
178 | + startDate: 'calculateDayCount', | |
179 | + startTime: 'calculateDayCount', | |
180 | + endDate: 'calculateDayCount', | |
181 | + endTime: 'calculateDayCount', | |
182 | + reason: "validateForm", | |
183 | + category: 'category', | |
184 | + }, | |
185 | +}; | |
186 | +</script> |
--- client/views/pages/Manager/approval/HyugaInsert.vue
+++ client/views/pages/Manager/approval/HyugaInsert.vue
... | ... | @@ -1,100 +1,129 @@ |
1 | 1 |
<template> |
2 |
- <div class="pagetitle"> |
|
3 |
- <h2>휴가신청</h2> |
|
4 |
- </div><!-- End Page Title --> |
|
2 |
+ <div class="card"> |
|
5 | 3 |
|
6 |
- <section class="section"> |
|
7 |
- <div class="card"> |
|
8 |
- <div class="card-body"> |
|
4 |
+ <div class="card-body"> |
|
5 |
+ <h2 class="card-title">휴가 신청</h2> |
|
6 |
+ <p class="require"><img :src="require" alt=""> 필수입력</p> |
|
7 |
+ <!-- Multi Columns Form --> |
|
8 |
+ <form class="row g-3 needs-validation" @submit.prevent="handleSubmit"> |
|
9 |
+ <div class="col-12"> |
|
10 |
+ <label for="inputName5" class="form-label">유형<p class="require"><img :src="require" alt=""></p></label> |
|
11 |
+ <select id="category" class="form-select" v-model="category" style="max-width: 200px;"> |
|
12 |
+ <option selected>연차</option> |
|
13 |
+ <option>반차</option> |
|
14 |
+ <option>병가</option> |
|
15 |
+ <option>경조</option> |
|
16 |
+ <option>무급</option> |
|
17 |
+ <option>공가</option> |
|
18 |
+ </select> |
|
19 |
+ </div> |
|
9 | 20 |
|
10 |
- <!-- Multi Columns Form --> |
|
11 |
- <form class="row g-3 pt-3" @submit.prevent="handleSubmit"> |
|
12 |
- <div class="col-md-9"> |
|
13 |
- <label for="inputName5" class="form-label">구분</label> |
|
14 |
- <select id="category" class="form-select" v-model="category"> |
|
15 |
- <option selected>연차</option> |
|
16 |
- <option>반차</option> |
|
17 |
- <option>병가</option> |
|
18 |
- <option>경조</option> |
|
19 |
- <option>무급</option> |
|
20 |
- <option>공가</option> |
|
21 |
+ <div class="col-12"> |
|
22 |
+ <label for="startDate" class="form-label">시작일<p class="require"><img :src="require" alt=""></p></label> |
|
23 |
+ <div class="d-flex gap-1"> |
|
24 |
+ <input type="date" class="form-control" id="startDate" v-model="startDate" /> |
|
25 |
+ <!-- 시간 선택을 위한 select 사용 --> |
|
26 |
+ <select class="form-control" id="startTime" v-model="startTime"> |
|
27 |
+ <option value="09:00">09:00</option> |
|
28 |
+ <option value="10:00">10:00</option> |
|
29 |
+ <option value="11:00">11:00</option> |
|
30 |
+ <option value="12:00">12:00</option> |
|
31 |
+ <option value="13:00">13:00</option> |
|
32 |
+ <option value="14:00">14:00</option> |
|
33 |
+ <option value="15:00">15:00</option> |
|
34 |
+ <option value="16:00">16:00</option> |
|
35 |
+ <option value="17:00">17:00</option> |
|
36 |
+ <option value="18:00">18:00</option> |
|
21 | 37 |
</select> |
22 | 38 |
</div> |
39 |
+ </div> |
|
23 | 40 |
|
24 |
- <div class="col-md-5"> |
|
25 |
- <label for="startDate" class="form-label">시작일</label> |
|
26 |
- <div class="d-flex gap-1"> |
|
27 |
- <input type="date" class="form-control" id="startDate" v-model="startDate" /> |
|
28 |
- <!-- 시간 선택을 위한 select 사용 --> |
|
29 |
- <select class="form-control" id="startTime" v-model="startTime"> |
|
30 |
- <option value="09:00">09:00</option> |
|
31 |
- <option value="10:00">10:00</option> |
|
32 |
- <option value="11:00">11:00</option> |
|
33 |
- <option value="12:00">12:00</option> |
|
34 |
- <option value="13:00">13:00</option> |
|
35 |
- <option value="14:00">14:00</option> |
|
36 |
- <option value="15:00">15:00</option> |
|
37 |
- <option value="16:00">16:00</option> |
|
38 |
- <option value="17:00">17:00</option> |
|
39 |
- <option value="18:00">18:00</option> |
|
41 |
+ <div class="col-12"> |
|
42 |
+ <label for="endDate" class="form-label">종료일<p class="require"><img :src="require" alt=""></p></label> |
|
43 |
+ <div class="d-flex gap-1"> |
|
44 |
+ <input type="date" class="form-control" id="endDate" v-model="endDate" /> |
|
45 |
+ <!-- 종료 시간을 위한 select 사용 --> |
|
46 |
+ <select class="form-control" id="endTime" v-model="endTime"> |
|
47 |
+ <option value="09:00">09:00</option> |
|
48 |
+ <option value="10:00">10:00</option> |
|
49 |
+ <option value="11:00">11:00</option> |
|
50 |
+ <option value="12:00">12:00</option> |
|
51 |
+ <option value="13:00">13:00</option> |
|
52 |
+ <option value="14:00">14:00</option> |
|
53 |
+ <option value="15:00">15:00</option> |
|
54 |
+ <option value="16:00">16:00</option> |
|
55 |
+ <option value="17:00">17:00</option> |
|
56 |
+ <option value="18:00">18:00</option> |
|
57 |
+ </select> |
|
58 |
+ </div> |
|
59 |
+ </div> |
|
60 |
+ |
|
61 |
+ <div class="col-12"> |
|
62 |
+ <label for="dayCount" class="form-label">사용 휴가일</label> |
|
63 |
+ <input type="text" class="form-control" id="dayCount" v-model="dayCount" readonly /> |
|
64 |
+ </div> |
|
65 |
+ |
|
66 |
+ <div class="col-12"> |
|
67 |
+ <label for="member" class="form-label"> |
|
68 |
+ 승인자 |
|
69 |
+ <button type="button" title="추가" @click="addApproval"> |
|
70 |
+ <PlusCircleFilled /> |
|
71 |
+ </button> |
|
72 |
+ </label> |
|
73 |
+ |
|
74 |
+ <!-- 반복 렌더링되는 addapproval 항목 --> |
|
75 |
+ <div class="approval-container"> |
|
76 |
+ <div v-for="(approval, index) in approvals" :key="index" class="d-flex gap-2 addapproval mb-2"> |
|
77 |
+ <select class="form-select" v-model="approval.category" style="width: 110px;"> |
|
78 |
+ <option value="결재">결재</option> |
|
79 |
+ <option value="전결">전결</option> |
|
80 |
+ <option value="대결">대결</option> |
|
40 | 81 |
</select> |
82 |
+ |
|
83 |
+ <form class="d-flex align-items-center border-x"> |
|
84 |
+ <input type="text" class="form-control" v-model="approval.name" style="max-width: 150px;" /> |
|
85 |
+ <button type="button" @click="removeApproval(index)" class="delete-button"> |
|
86 |
+ <CloseOutlined /> |
|
87 |
+ </button> |
|
88 |
+ </form> |
|
41 | 89 |
</div> |
42 | 90 |
</div> |
91 |
+ </div> |
|
92 |
+ <div class="col-12 border-x hyuga"> |
|
93 |
+ <label for="prvonsh" class="form-label">세부사항</label> |
|
94 |
+ <input type="text" class="form-control textarea" id="reason" v-model="reason" /> |
|
95 |
+ </div> |
|
43 | 96 |
|
44 |
- <div class="col-md-5"> |
|
45 |
- <label for="endDate" class="form-label">종료일</label> |
|
46 |
- <div class="d-flex gap-1"> |
|
47 |
- <input type="date" class="form-control" id="endDate" v-model="endDate" /> |
|
48 |
- <!-- 종료 시간을 위한 select 사용 --> |
|
49 |
- <select class="form-control" id="endTime" v-model="endTime"> |
|
50 |
- <option value="09:00">09:00</option> |
|
51 |
- <option value="10:00">10:00</option> |
|
52 |
- <option value="11:00">11:00</option> |
|
53 |
- <option value="12:00">12:00</option> |
|
54 |
- <option value="13:00">13:00</option> |
|
55 |
- <option value="14:00">14:00</option> |
|
56 |
- <option value="15:00">15:00</option> |
|
57 |
- <option value="16:00">16:00</option> |
|
58 |
- <option value="17:00">17:00</option> |
|
59 |
- <option value="18:00">18:00</option> |
|
60 |
- </select> |
|
61 |
- </div> |
|
62 |
- </div> |
|
63 | 97 |
|
64 |
- <div class="col-md-4"> |
|
65 |
- <label for="dayCount" class="form-label">사용 휴가일</label> |
|
66 |
- <input type="text" class="form-control" id="dayCount" v-model="dayCount" readonly /> |
|
67 |
- </div> |
|
68 |
- |
|
69 |
- <div class="col-12"> |
|
70 |
- <label for="prvonsh" class="form-label">사유</label> |
|
71 |
- <input type="text" class="form-control" id="reason" v-model="reason" /> |
|
72 |
- </div> |
|
73 |
- |
|
74 |
- <div class="text-end"> |
|
75 |
- <button type="submit" class="btn btn-primary">승인요청</button> |
|
76 |
- <button type="reset" class="btn btn-secondary">취소</button> |
|
77 |
- </div> |
|
78 |
- </form><!-- End Multi Columns Form --> |
|
79 |
- |
|
98 |
+ </form><!-- End Multi Columns Form --> |
|
99 |
+ <div class="buttons"> |
|
100 |
+ <button type="submit" class="btn btn-red">이전 승인자 불러오기</button> |
|
101 |
+ <button type="submit" class="btn primary">신청</button> |
|
102 |
+ <button type="reset" class="btn tertiary">취소</button> |
|
80 | 103 |
</div> |
81 | 104 |
</div> |
82 |
- </section> |
|
105 |
+ </div> |
|
83 | 106 |
</template> |
84 | 107 |
|
85 | 108 |
<script> |
109 |
+import { PlusCircleFilled, CloseOutlined } from '@ant-design/icons-vue'; |
|
86 | 110 |
export default { |
87 | 111 |
data() { |
88 | 112 |
const today = new Date().toISOString().split('T')[0]; |
89 | 113 |
return { |
114 |
+ approvals: [], |
|
115 |
+ require: "/client/resources/img/require.png", |
|
90 | 116 |
startDate: today, |
91 | 117 |
startTime: "09:00", // 기본 시작 시간 09:00 |
92 | 118 |
endDate: today, |
93 | 119 |
endTime: "18:00", // 기본 종료 시간 18:00 |
94 |
- category: "", |
|
95 |
- dayCount: 1, |
|
120 |
+ category: "", |
|
121 |
+ dayCount: 1, |
|
96 | 122 |
reason: "", // 사유 |
97 | 123 |
}; |
124 |
+ }, |
|
125 |
+ components: { |
|
126 |
+ PlusCircleFilled, CloseOutlined |
|
98 | 127 |
}, |
99 | 128 |
computed: { |
100 | 129 |
// Pinia Store의 상태를 가져옵니다. |
... | ... | @@ -104,7 +133,16 @@ |
104 | 133 |
}, |
105 | 134 |
}, |
106 | 135 |
methods: { |
107 |
- // 폼 검증 메서드 |
|
136 |
+ addApproval() { |
|
137 |
+ this.approvals.push({ |
|
138 |
+ category: '결재', |
|
139 |
+ name: '', |
|
140 |
+ }); |
|
141 |
+ }, |
|
142 |
+ // 승인자 삭제 |
|
143 |
+ removeApproval(index) { |
|
144 |
+ this.approvals.splice(index, 1); |
|
145 |
+ }, |
|
108 | 146 |
validateForm() { |
109 | 147 |
// 필수 입력 필드 체크 |
110 | 148 |
if ( |
... | ... | @@ -122,32 +160,32 @@ |
122 | 160 |
} |
123 | 161 |
}, |
124 | 162 |
calculateDayCount() { |
125 |
- const start = new Date(`${this.startDate}T${this.startTime}:00`); |
|
126 |
- const end = new Date(`${this.endDate}T${this.endTime}:00`); |
|
127 |
- |
|
128 |
- let totalHours = (end - start) / (1000 * 60 * 60); // 밀리초를 시간 단위로 변환 |
|
129 |
- |
|
130 |
- if (this.startDate !== this.endDate) { |
|
131 |
- // 시작일과 종료일이 다른경우 |
|
132 |
- const startDateObj = new Date(this.startDate); |
|
133 |
- const endDateObj = new Date(this.endDate); |
|
134 |
- const daysDifference = (endDateObj - startDateObj) / (1000 * 60 * 60 * 24); // 두 날짜 사이의 차이를 일수로 계산 |
|
135 |
- if (this.startTime !== "09:00" || this.endTime !== "18:00") { |
|
136 |
- this.dayCount = daysDifference + 0.5; // 시간 조건이 기준에서 벗어날 경우 |
|
137 |
- } else { |
|
138 |
- this.dayCount = Math.ceil(daysDifference + 1); // 시간 조건이 기준에 맞을 경우 |
|
139 |
- } |
|
140 |
- } else { |
|
141 |
- // 시작일과 종료일이 같은 경우 |
|
142 |
- if (this.startTime !== "09:00" || this.endTime !== "18:00") { |
|
143 |
- this.dayCount = 0.5; // 시작 시간 또는 종료 시간이 기준과 다를 경우 0.5 |
|
144 |
- } else { |
|
145 |
- this.dayCount = 1; // 기준 시간(09:00~18:00)이 맞으면 1일로 간주 |
|
146 |
- } |
|
147 |
- } |
|
163 |
+ const start = new Date(`${this.startDate}T${this.startTime}:00`); |
|
164 |
+ const end = new Date(`${this.endDate}T${this.endTime}:00`); |
|
148 | 165 |
|
149 |
- this.validateForm(); // dayCount 변경 후 폼 재검증 |
|
150 |
- }, |
|
166 |
+ let totalHours = (end - start) / (1000 * 60 * 60); // 밀리초를 시간 단위로 변환 |
|
167 |
+ |
|
168 |
+ if (this.startDate !== this.endDate) { |
|
169 |
+ // 시작일과 종료일이 다른경우 |
|
170 |
+ const startDateObj = new Date(this.startDate); |
|
171 |
+ const endDateObj = new Date(this.endDate); |
|
172 |
+ const daysDifference = (endDateObj - startDateObj) / (1000 * 60 * 60 * 24); // 두 날짜 사이의 차이를 일수로 계산 |
|
173 |
+ if (this.startTime !== "09:00" || this.endTime !== "18:00") { |
|
174 |
+ this.dayCount = daysDifference + 0.5; // 시간 조건이 기준에서 벗어날 경우 |
|
175 |
+ } else { |
|
176 |
+ this.dayCount = Math.ceil(daysDifference + 1); // 시간 조건이 기준에 맞을 경우 |
|
177 |
+ } |
|
178 |
+ } else { |
|
179 |
+ // 시작일과 종료일이 같은 경우 |
|
180 |
+ if (this.startTime !== "09:00" || this.endTime !== "18:00") { |
|
181 |
+ this.dayCount = 0.5; // 시작 시간 또는 종료 시간이 기준과 다를 경우 0.5 |
|
182 |
+ } else { |
|
183 |
+ this.dayCount = 1; // 기준 시간(09:00~18:00)이 맞으면 1일로 간주 |
|
184 |
+ } |
|
185 |
+ } |
|
186 |
+ |
|
187 |
+ this.validateForm(); // dayCount 변경 후 폼 재검증 |
|
188 |
+ }, |
|
151 | 189 |
handleSubmit() { |
152 | 190 |
this.validateForm(); // 제출 시 유효성 확인 |
153 | 191 |
if (this.isFormValid) { |
... | ... | @@ -158,26 +196,17 @@ |
158 | 196 |
alert("모든 필드를 올바르게 작성해주세요."); |
159 | 197 |
} |
160 | 198 |
}, |
161 |
- loadFormData() { |
|
162 |
- const savedData = localStorage.getItem('HyugaFormData'); |
|
163 |
- if (savedData) { |
|
164 |
- this.$data = JSON.parse(savedData); |
|
165 |
- } |
|
166 |
- console.log(loadFormData) |
|
167 |
- }, |
|
168 |
- |
|
199 |
+ |
|
169 | 200 |
}, |
170 | 201 |
mounted() { |
171 |
- // Load the saved form data when the page is loaded |
|
172 |
- this.loadFormData(); |
|
173 | 202 |
}, |
174 | 203 |
watch: { |
175 |
- startDate: 'calculateDayCount', |
|
176 |
- startTime: 'calculateDayCount', |
|
177 |
- endDate: 'calculateDayCount', |
|
178 |
- endTime: 'calculateDayCount', |
|
179 |
- reason: "validateForm", |
|
180 |
- category: 'category', |
|
204 |
+ startDate: 'calculateDayCount', |
|
205 |
+ startTime: 'calculateDayCount', |
|
206 |
+ endDate: 'calculateDayCount', |
|
207 |
+ endTime: 'calculateDayCount', |
|
208 |
+ reason: "validateForm", |
|
209 |
+ category: 'category', |
|
181 | 210 |
}, |
182 | 211 |
}; |
183 | 212 |
</script> |
--- client/views/pages/Manager/approval/approval.vue
+++ client/views/pages/Manager/approval/approval.vue
... | ... | @@ -1,46 +1,46 @@ |
1 | 1 |
<template> |
2 | 2 |
<div class="sidemenu"> |
3 | 3 |
<div class="myinfo simple"> |
4 |
- <div class="name-box"> |
|
5 |
- <div class="img-area"> |
|
6 |
- <div><img :src="photoicon" alt=""> |
|
7 |
- <p class="name">OOO과장</p> |
|
8 |
- </div> |
|
9 |
- <div class="info"> |
|
10 |
- <p>솔루션 개발팀</p> |
|
11 |
- <i class="fa-bars"></i> |
|
12 |
- <p>팀장</p> |
|
13 |
- </div> |
|
4 |
+ <div class="name-box"> |
|
5 |
+ <div class="img-area"> |
|
6 |
+ <div><img :src="photoicon" alt=""> |
|
7 |
+ <p class="name">OOO과장</p> |
|
8 |
+ </div> |
|
9 |
+ <div class="info"> |
|
10 |
+ <p>솔루션 개발팀</p> |
|
11 |
+ <i class="fa-bars"></i> |
|
12 |
+ <p>팀장</p> |
|
13 |
+ </div> |
|
14 |
+ </div> |
|
15 |
+ </div> |
|
16 |
+ |
|
17 |
+ |
|
18 |
+ <details class="menu-box"> |
|
19 |
+ <summary><p>결재</p><div class="icon"><img :src="topmenuicon" alt=""></div></summary> |
|
20 |
+ <ul> |
|
21 |
+ <li> <router-link to="/approvalRequest.page" exact-active-class="active-link" v-slot="{ isExactActive }"> |
|
22 |
+ <p>결재 요청</p> |
|
23 |
+ <div class="icon" v-if="isExactActive"> |
|
24 |
+ <img :src="menuicon" alt=""> |
|
14 | 25 |
</div> |
15 |
- </div> |
|
16 |
- |
|
17 |
- <h2>결재</h2> |
|
18 |
- <ul class="menu-box"> |
|
19 |
- <li> |
|
20 |
- <router-link to="/approvalRequest.page" exact-active-class="active-link" v-slot="{ isExactActive }"> |
|
21 |
- <p>결재 요청</p> |
|
22 |
- <div class="icon" v-if="isExactActive"> |
|
23 |
- <img :src="menuicon" alt=""> |
|
24 |
- </div> |
|
25 |
- </router-link> |
|
26 |
- |
|
27 |
- </li> |
|
28 |
- <li> |
|
29 |
- <router-link to="/approvalList.page" exact-active-class="active-link" v-slot="{ isExactActive }"> |
|
30 |
- <p>승인 대기 목록</p> |
|
31 |
- <div class="icon" v-if="isExactActive"> |
|
32 |
- <img :src="menuicon" alt=""> |
|
33 |
- </div> |
|
34 |
- </router-link> |
|
35 |
- </li> |
|
36 |
- </ul> |
|
37 |
- </div> |
|
26 |
+ </router-link></li> |
|
27 |
+ <li> |
|
28 |
+ <router-link to="/approvalList.page" exact-active-class="active-link" v-slot="{ isExactActive }"> |
|
29 |
+ <p>승인 대기 목록</p> |
|
30 |
+ <div class="icon" v-if="isExactActive"> |
|
31 |
+ <img :src="menuicon" alt=""> |
|
32 |
+ </div> |
|
33 |
+ </router-link> |
|
34 |
+ </li> |
|
35 |
+ </ul> |
|
36 |
+ </details> |
|
37 |
+ </div> |
|
38 | 38 |
</div> |
39 | 39 |
<!-- End Page Title --> |
40 | 40 |
<div class="content"> |
41 | 41 |
<router-view></router-view> |
42 | 42 |
|
43 |
-</div> |
|
43 |
+ </div> |
|
44 | 44 |
</template> |
45 | 45 |
|
46 | 46 |
<script> |
... | ... | @@ -51,6 +51,7 @@ |
51 | 51 |
return { |
52 | 52 |
photoicon: "/client/resources/img/photo_icon.png", |
53 | 53 |
menuicon: "/client/resources/img/menuicon.png", |
54 |
+ topmenuicon: "/client/resources/img/topmenuicon.png", |
|
54 | 55 |
// 데이터 초기화 |
55 | 56 |
years: [2023, 2024, 2025], // 연도 목록 |
56 | 57 |
months: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], // 월 목록 |
--- client/views/pages/Manager/approval/approvalRequest.vue
+++ client/views/pages/Manager/approval/approvalRequest.vue
... | ... | @@ -154,7 +154,7 @@ |
154 | 154 |
if (type === '휴가') { |
155 | 155 |
this.$router.push('/HyugaInsert.page'); |
156 | 156 |
} else if (type === '출장') { |
157 |
- this.$router.push('/ChuljangInsert.page'); |
|
157 |
+ this.$router.push('/ChuljangDetail.page'); |
|
158 | 158 |
} |
159 | 159 |
}, |
160 | 160 |
getStatusClass(status) { |
+++ client/views/pages/Manager/attendance/AttendanceDetail.vue
... | ... | @@ -0,0 +1,283 @@ |
1 | +<template> | |
2 | + <div class="card "> | |
3 | + <div class="card-body"> | |
4 | + <h2 class="card-title">부서별 근태현황</h2> | |
5 | + <div class="name-box flex sb simple"> | |
6 | + <div class="img-area"> | |
7 | + <div class="img"><img :src="photoicon" alt=""> | |
8 | + </div> | |
9 | + </div> | |
10 | + <form class="row g-3 needs-validation " :class="{ 'was-validated': formSubmitted }" | |
11 | + @submit.prevent="handleRegister" novalidate> | |
12 | + <div class="col-12"> | |
13 | + <label for="yourName" class="form-label">아이디</label> | |
14 | + <input v-model="name" type="text" name="name" class="form-control" id="yourName" required readonly | |
15 | + placeholder="admin"> | |
16 | + </div> | |
17 | + <div class="col-12 "> | |
18 | + <div class="col-12 border-x"> | |
19 | + <label for="youremail" class="form-label ">이름<p class="require"><img :src="require" alt=""></p></label> | |
20 | + <input v-model="email" type="text" name="username" class="form-control" id="youremail" required readonly> | |
21 | + </div> | |
22 | + | |
23 | + <div class="col-12 border-x"> | |
24 | + <label for="yourPassword" class="form-label">부서</label> | |
25 | + <input v-model="password" type="password" name="password" class="form-control" id="yourPassword" | |
26 | + required readonly placeholder="주식회사 테이큰 소프트"> | |
27 | + </div> | |
28 | + </div> | |
29 | + <div class="col-12 border-x"> | |
30 | + <div class="col-12 border-x"> | |
31 | + <label for="youremail" class="form-label">직급</label> | |
32 | + <input v-model="email" type="text" name="username" class="form-control" id="youremail" required readonly | |
33 | + placeholder="과장"> | |
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" id="yourPassword" | |
39 | + required readonly placeholder="팀장"> | |
40 | + </div> | |
41 | + </div> | |
42 | + | |
43 | + | |
44 | + </form> | |
45 | + </div> | |
46 | + | |
47 | + <div class="sch-form-wrap "> | |
48 | + <div class="input-group"> | |
49 | + <select name="" id="" class="form-select"> | |
50 | + <option value="">년도</option> | |
51 | + </select> | |
52 | + <select name="" id="" class="form-select"> | |
53 | + <option value="">월</option> | |
54 | + </select> | |
55 | + <select name="" id="" class="form-select"> | |
56 | + <option value="">구분</option> | |
57 | + </select> | |
58 | + </div> | |
59 | + | |
60 | + </div> | |
61 | + <div class=" tbl-wrap tbl2"> | |
62 | + <table class="tbl data"> | |
63 | + <colgroup> | |
64 | + <col style="width: 150px;"> | |
65 | + <col style="width: "> | |
66 | + <col style="width: "> | |
67 | + <col style="width: "> | |
68 | + <col style="width: "> | |
69 | + <col style="width: "> | |
70 | + <!-- 더 많은 열 설정 --> | |
71 | + </colgroup> | |
72 | + <tbody> | |
73 | + <tr class="thead"> | |
74 | + <td rowspan="2" class="th">근태 현황</td> | |
75 | + <td>지각</td> | |
76 | + <td>조기퇴근</td> | |
77 | + <td>결근</td> | |
78 | + <td>출장</td> | |
79 | + <td>주말출근</td> | |
80 | + </tr> | |
81 | + <tr> | |
82 | + <td>{{ late }}</td> | |
83 | + <td>{{ earlyLeave }}</td> | |
84 | + <td>{{ absence }}</td> | |
85 | + <td>{{ businessTrip }}</td> | |
86 | + <td>{{ weekendWork }}</td> | |
87 | + </tr> | |
88 | + </tbody> | |
89 | + | |
90 | + </table> | |
91 | + <table class="tbl data"> | |
92 | + <colgroup> | |
93 | + <col style="width: 150px;"> | |
94 | + <col span=""> | |
95 | + <col style="width: "> | |
96 | + <col style="width: "> | |
97 | + <col style="width: "> | |
98 | + <!-- 더 많은 열 설정 --> | |
99 | + </colgroup> | |
100 | + <tbody> | |
101 | + <tr class="thead"> | |
102 | + <td rowspan="2" class="th">휴가 현황</td> | |
103 | + <td>연차</td> | |
104 | + <td>대체휴가</td> | |
105 | + <td>공가</td> | |
106 | + <td>병가</td> | |
107 | + </tr> | |
108 | + <tr> | |
109 | + <td></td> | |
110 | + <td></td> | |
111 | + <td></td> | |
112 | + <td></td> | |
113 | + </tr> | |
114 | + </tbody> | |
115 | + | |
116 | + </table> | |
117 | + </div> | |
118 | + <div class="tbl-wrap"> | |
119 | + <table id="myTable" class="tbl data"> | |
120 | + <colgroup> | |
121 | + <col style="width: 200px;"> | |
122 | + <col style=" width: "> | |
123 | + </colgroup> | |
124 | + <thead> | |
125 | + <tr> | |
126 | + <th>연차 </th> | |
127 | + <th>내용</th> | |
128 | + </tr> | |
129 | + </thead> | |
130 | + <!-- 동적으로 <td> 생성 --> | |
131 | + <tbody> | |
132 | + <tr v-for="(item, index) in listData" :key="index"> | |
133 | + <td>{{ item.type }}</td> | |
134 | + <td>{{ item.content }}</td> | |
135 | + </tr> | |
136 | + </tbody> | |
137 | + </table> | |
138 | + | |
139 | + </div> | |
140 | + <div class="pagination"> | |
141 | + <ul> | |
142 | + <!-- 왼쪽 화살표 (이전 페이지) --> | |
143 | + <li class="arrow" :class="{ disabled: currentPage === 1 }" @click="changePage(currentPage - 1)"> | |
144 | + < | |
145 | + </li> | |
146 | + | |
147 | + <!-- 페이지 번호 --> | |
148 | + <li v-for="page in totalPages" :key="page" :class="{ active: currentPage === page }" | |
149 | + @click="changePage(page)"> | |
150 | + {{ page }} | |
151 | + </li> | |
152 | + | |
153 | + <!-- 오른쪽 화살표 (다음 페이지) --> | |
154 | + <li class="arrow" :class="{ disabled: currentPage === totalPages }" @click="changePage(currentPage + 1)"> | |
155 | + > | |
156 | + </li> | |
157 | + </ul> | |
158 | + </div> | |
159 | + | |
160 | + </div> | |
161 | + </div> | |
162 | +</template> | |
163 | + | |
164 | +<script> | |
165 | +import { SearchOutlined } from '@ant-design/icons-vue'; | |
166 | + | |
167 | +export default { | |
168 | + data() { | |
169 | + | |
170 | + const today = new Date().toISOString().split('T')[0]; | |
171 | + return { | |
172 | + photoicon: "/client/resources/img/img1.png", | |
173 | + currentPage: 1, | |
174 | + totalPages: 3, | |
175 | + late: '5', earlyLeave: '3', absence: '2', businessTrip: '1', weekendWork: '0', | |
176 | + today: new Date().toLocaleDateString('ko-KR', { | |
177 | + year: 'numeric', | |
178 | + month: '2-digit', | |
179 | + day: '2-digit', | |
180 | + weekday: 'short', | |
181 | + }), | |
182 | + dateicon: "/client/resources/img/img.png", | |
183 | + startbtn: "/client/resources/img/start-sm.png", | |
184 | + stopbtn: "/client/resources/img/stop-sm.png", | |
185 | + startDate: today, | |
186 | + startTime: "09:00", // 기본 시작 시간 09:00 | |
187 | + endDate: today, | |
188 | + endTime: "18:00", // 기본 종료 시간 18:00 | |
189 | + category: "", | |
190 | + dayCount: 1, | |
191 | + reason: "", // 사유 | |
192 | + listData: [ | |
193 | + { | |
194 | + type: '연차', | |
195 | + content: '결재', | |
196 | + }, { | |
197 | + type: '반차', | |
198 | + content: '전결', | |
199 | + }], | |
200 | + }; | |
201 | + }, | |
202 | + components: { | |
203 | + SearchOutlined | |
204 | + }, | |
205 | + computed: { | |
206 | + // Pinia Store의 상태를 가져옵니다. | |
207 | + loginUser() { | |
208 | + const authStore = useAuthStore(); | |
209 | + return authStore.getLoginUser; | |
210 | + }, | |
211 | + }, | |
212 | + methods: { | |
213 | + // 폼 검증 메서드 | |
214 | + validateForm() { | |
215 | + // 필수 입력 필드 체크 | |
216 | + if ( | |
217 | + this.category && | |
218 | + this.startDate && | |
219 | + this.startTime && | |
220 | + this.endDate && | |
221 | + this.endTime && | |
222 | + this.dayCount > 0 && | |
223 | + this.reason.trim() !== "" | |
224 | + ) { | |
225 | + this.isFormValid = true; | |
226 | + } else { | |
227 | + this.isFormValid = false; | |
228 | + } | |
229 | + }, | |
230 | + calculateDayCount() { | |
231 | + const start = new Date(`${this.startDate}T${this.startTime}:00`); | |
232 | + const end = new Date(`${this.endDate}T${this.endTime}:00`); | |
233 | + | |
234 | + let totalHours = (end - start) / (1000 * 60 * 60); // 밀리초를 시간 단위로 변환 | |
235 | + | |
236 | + if (this.startDate !== this.endDate) { | |
237 | + // 시작일과 종료일이 다른경우 | |
238 | + const startDateObj = new Date(this.startDate); | |
239 | + const endDateObj = new Date(this.endDate); | |
240 | + const daysDifference = (endDateObj - startDateObj) / (1000 * 60 * 60 * 24); // 두 날짜 사이의 차이를 일수로 계산 | |
241 | + if (this.startTime !== "09:00" || this.endTime !== "18:00") { | |
242 | + this.dayCount = daysDifference + 0.5; // 시간 조건이 기준에서 벗어날 경우 | |
243 | + } else { | |
244 | + this.dayCount = Math.ceil(daysDifference + 1); // 시간 조건이 기준에 맞을 경우 | |
245 | + } | |
246 | + } else { | |
247 | + // 시작일과 종료일이 같은 경우 | |
248 | + if (this.startTime !== "09:00" || this.endTime !== "18:00") { | |
249 | + this.dayCount = 0.5; // 시작 시간 또는 종료 시간이 기준과 다를 경우 0.5 | |
250 | + } else { | |
251 | + this.dayCount = 1; // 기준 시간(09:00~18:00)이 맞으면 1일로 간주 | |
252 | + } | |
253 | + } | |
254 | + | |
255 | + this.validateForm(); // dayCount 변경 후 폼 재검증 | |
256 | + }, | |
257 | + handleSubmit() { | |
258 | + this.validateForm(); // 제출 시 유효성 확인 | |
259 | + if (this.isFormValid) { | |
260 | + localStorage.setItem('HyugaFormData', JSON.stringify(this.$data)); | |
261 | + alert("승인 요청이 완료되었습니다."); | |
262 | + // 추가 처리 로직 (API 요청 등) | |
263 | + } else { | |
264 | + alert("모든 필드를 올바르게 작성해주세요."); | |
265 | + } | |
266 | + }, | |
267 | + | |
268 | + | |
269 | + }, | |
270 | + mounted() { | |
271 | + // Load the saved form data when the page is loaded | |
272 | + this.loadFormData(); | |
273 | + }, | |
274 | + watch: { | |
275 | + startDate: 'calculateDayCount', | |
276 | + startTime: 'calculateDayCount', | |
277 | + endDate: 'calculateDayCount', | |
278 | + endTime: 'calculateDayCount', | |
279 | + reason: "validateForm", | |
280 | + category: 'category', | |
281 | + }, | |
282 | +}; | |
283 | +</script> |
+++ client/views/pages/Manager/attendance/ChuljangBokmyeongDetail.vue
... | ... | @@ -0,0 +1,174 @@ |
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 " :class="{ 'was-validated': formSubmitted }" @submit.prevent="handleRegister" novalidate> | |
25 | + | |
26 | + <div class="col-12 chuljang"> | |
27 | + <label for="yourName" class="form-label">복명내용</label> | |
28 | + <input v-model="name" type="text" name="name" class="form-control textarea " id="yourName" readonly> | |
29 | + </div> | |
30 | + <div class="col-12"> | |
31 | + <label for="yourName" class="form-label">여비계산</label> | |
32 | + <input v-model="name" type="text" name="name" class="form-control " id="yourName" readonly> | |
33 | + </div> | |
34 | + <div class="col-12 "> | |
35 | + <label for="yourName" class="form-label">복명 신청일</label> | |
36 | + <input v-model="name" type="text" name="name" class="form-control" id="yourName" readonly placeholder="2025-01-01"> | |
37 | + </div> | |
38 | + <div class="col-12 border-x " :class="{ return: isReturned }"> | |
39 | + <label for="yourName" class="form-label">반려사유</label> | |
40 | + <input v-model="name" type="text" name="name" class="form-control" id="yourName" readonly placeholder="2025-01-01"> | |
41 | + </div> | |
42 | + | |
43 | + | |
44 | + </form> | |
45 | + </div> | |
46 | + <div class="buttons"> | |
47 | + <button class="btn btn-red " type="submit">신청취소</button> | |
48 | + <button class="btn secondary" type="submit" @click="handleButtonClick">{{ isReturned ? '재신청' : '수정' }}</button> | |
49 | + <button class="btn tertiary " type="submit">목록</button> | |
50 | + </div> | |
51 | + | |
52 | + </div> | |
53 | + </div> | |
54 | +</template> | |
55 | + | |
56 | +<script> | |
57 | +export default { | |
58 | + data() { | |
59 | + const today = new Date().toISOString().split('T')[0]; | |
60 | + return { | |
61 | + isReturned: true, | |
62 | + startDate: today, | |
63 | + startTime: "09:00", // 기본 시작 시간 09:00 | |
64 | + endDate: today, | |
65 | + endTime: "18:00", // 기본 종료 시간 18:00 | |
66 | + category: "", | |
67 | + dayCount: 1, | |
68 | + reason: "", // 사유 | |
69 | + listData: [ | |
70 | + { | |
71 | + type: '연차', | |
72 | + approvalType: '결재', | |
73 | + applicant: '홍길동', | |
74 | + period: '2025-05-10 ~ 2025-15-03', | |
75 | + requestDate: '2025-04-25', | |
76 | + status: '대기' | |
77 | + }, { | |
78 | + type: '반차', | |
79 | + approvalType: '전결', | |
80 | + applicant: '홍길동', | |
81 | + period: '2025-05-01 ~ 2025-05-03', | |
82 | + requestDate: '2025-04-25', | |
83 | + status: '승인' | |
84 | + }], | |
85 | + }; | |
86 | + }, | |
87 | + computed: { | |
88 | + // Pinia Store의 상태를 가져옵니다. | |
89 | + loginUser() { | |
90 | + const authStore = useAuthStore(); | |
91 | + return authStore.getLoginUser; | |
92 | + }, | |
93 | + }, | |
94 | + methods: { | |
95 | + // 폼 검증 메서드 | |
96 | + handleButtonClick() { | |
97 | + if (this.isReturned) { | |
98 | + // If "재신청", navigate to the desired page (ChuljangInsert.page) | |
99 | + this.$router.push({ name: 'ChuljangInsert' }); // Replace with the correct route name | |
100 | + } else { | |
101 | + // Handle the "수정" behavior here | |
102 | + console.log('수정 button clicked'); | |
103 | + } | |
104 | + }, | |
105 | + validateForm() { | |
106 | + // 필수 입력 필드 체크 | |
107 | + if ( | |
108 | + this.category && | |
109 | + this.startDate && | |
110 | + this.startTime && | |
111 | + this.endDate && | |
112 | + this.endTime && | |
113 | + this.dayCount > 0 && | |
114 | + this.reason.trim() !== "" | |
115 | + ) { | |
116 | + this.isFormValid = true; | |
117 | + } else { | |
118 | + this.isFormValid = false; | |
119 | + } | |
120 | + }, | |
121 | + calculateDayCount() { | |
122 | + const start = new Date(`${this.startDate}T${this.startTime}:00`); | |
123 | + const end = new Date(`${this.endDate}T${this.endTime}:00`); | |
124 | + | |
125 | + let totalHours = (end - start) / (1000 * 60 * 60); // 밀리초를 시간 단위로 변환 | |
126 | + | |
127 | + if (this.startDate !== this.endDate) { | |
128 | + // 시작일과 종료일이 다른경우 | |
129 | + const startDateObj = new Date(this.startDate); | |
130 | + const endDateObj = new Date(this.endDate); | |
131 | + const daysDifference = (endDateObj - startDateObj) / (1000 * 60 * 60 * 24); // 두 날짜 사이의 차이를 일수로 계산 | |
132 | + if (this.startTime !== "09:00" || this.endTime !== "18:00") { | |
133 | + this.dayCount = daysDifference + 0.5; // 시간 조건이 기준에서 벗어날 경우 | |
134 | + } else { | |
135 | + this.dayCount = Math.ceil(daysDifference + 1); // 시간 조건이 기준에 맞을 경우 | |
136 | + } | |
137 | + } else { | |
138 | + // 시작일과 종료일이 같은 경우 | |
139 | + if (this.startTime !== "09:00" || this.endTime !== "18:00") { | |
140 | + this.dayCount = 0.5; // 시작 시간 또는 종료 시간이 기준과 다를 경우 0.5 | |
141 | + } else { | |
142 | + this.dayCount = 1; // 기준 시간(09:00~18:00)이 맞으면 1일로 간주 | |
143 | + } | |
144 | + } | |
145 | + | |
146 | + this.validateForm(); // dayCount 변경 후 폼 재검증 | |
147 | + }, | |
148 | + handleSubmit() { | |
149 | + this.validateForm(); // 제출 시 유효성 확인 | |
150 | + if (this.isFormValid) { | |
151 | + localStorage.setItem('HyugaFormData', JSON.stringify(this.$data)); | |
152 | + alert("승인 요청이 완료되었습니다."); | |
153 | + // 추가 처리 로직 (API 요청 등) | |
154 | + } else { | |
155 | + alert("모든 필드를 올바르게 작성해주세요."); | |
156 | + } | |
157 | + }, | |
158 | + | |
159 | + | |
160 | + }, | |
161 | + mounted() { | |
162 | + // Load the saved form data when the page is loaded | |
163 | + this.loadFormData(); | |
164 | + }, | |
165 | + watch: { | |
166 | + startDate: 'calculateDayCount', | |
167 | + startTime: 'calculateDayCount', | |
168 | + endDate: 'calculateDayCount', | |
169 | + endTime: 'calculateDayCount', | |
170 | + reason: "validateForm", | |
171 | + category: 'category', | |
172 | + }, | |
173 | +}; | |
174 | +</script> |
+++ client/views/pages/Manager/attendance/ChuljangDetailAll.vue
... | ... | @@ -0,0 +1,232 @@ |
1 | +<template> | |
2 | +<div class="card "> | |
3 | + <div class="card-body"> | |
4 | + <h2 class="card-title">출장 현황</h2> | |
5 | + | |
6 | + <div class="form-card" style="margin-bottom: 20px;"> | |
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 " :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" id="youremail" 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" id="yourPassword" 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" id="youremail" 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" id="yourPassword" 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" id="yourName" readonly> | |
50 | + </div> | |
51 | + <div class="col-12"> | |
52 | + <label for="yourName" class="form-label">출장목적</label> | |
53 | + <input v-model="name" type="text" name="name" class="form-control " id="yourName" 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 " id="yourName" readonly> | |
58 | + </div> | |
59 | + <div class="col-12 chuljang"> | |
60 | + <label for="yourName" class="form-label">품의내용</label> | |
61 | + <input v-model="name" type="text" name="name" class="form-control textarea " id="yourName" readonly> | |
62 | + </div> | |
63 | + <div class="col-12"> | |
64 | + <label for="yourName" class="form-label">법인카드</label> | |
65 | + <input v-model="name" type="text" name="name" class="form-control " id="yourName" readonly> | |
66 | + </div> | |
67 | + <div class="col-12"> | |
68 | + <label for="yourName" class="form-label">법인차량</label> | |
69 | + <input v-model="name" type="text" name="name" class="form-control " id="yourName" readonly> | |
70 | + </div> | |
71 | + <div class="col-12 border-x"> | |
72 | + <label for="yourName" class="form-label">품의 신청일</label> | |
73 | + <input v-model="name" type="text" name="name" class="form-control " id="yourName" readonly> | |
74 | + </div> | |
75 | + </form> | |
76 | + </div> | |
77 | + <div class="form-card"> | |
78 | + <h1>출장복명서</h1> | |
79 | + <div class="approval-box tbl-wrap tbl2"> | |
80 | + <table class="tbl data"> | |
81 | + <tbody> | |
82 | + <tr class="thead"> | |
83 | + <td rowspan="2" class="th">승인자</td> | |
84 | + <td>과장</td> | |
85 | + <td>소장</td> | |
86 | + </tr> | |
87 | + <tr> | |
88 | + <td><p class="name">홍길동</p><p class="date">2025/05/09</p></td> | |
89 | + <td><p class="name">홍길동</p><p class="date">2025/05/09</p></td> | |
90 | + </tr> | |
91 | + </tbody> | |
92 | + | |
93 | + </table> | |
94 | + </div> | |
95 | + <form class="row g-3 needs-validation " :class="{ 'was-validated': formSubmitted }" @submit.prevent="handleRegister" novalidate> | |
96 | + | |
97 | + <div class="col-12 chuljang"> | |
98 | + <label for="yourName" class="form-label">복명내용</label> | |
99 | + <input v-model="name" type="text" name="name" class="form-control textarea " id="yourName" readonly> | |
100 | + </div> | |
101 | + <div class="col-12"> | |
102 | + <label for="yourName" class="form-label">여비계산</label> | |
103 | + <input v-model="name" type="text" name="name" class="form-control " id="yourName" readonly> | |
104 | + </div> | |
105 | + <div class="col-12 border-x"> | |
106 | + <label for="yourName" class="form-label">복명 신청일</label> | |
107 | + <input v-model="name" type="text" name="name" class="form-control" id="yourName" readonly placeholder="2025-01-01"> | |
108 | + </div> | |
109 | + | |
110 | + | |
111 | + </form> | |
112 | + </div> | |
113 | + <div class="buttons"> | |
114 | + <button class="btn btn-red " type="submit">신청취소</button> | |
115 | + <button class="btn secondary" type="submit"> 수정</button> | |
116 | + <button class="btn tertiary " type="submit">목록</button> | |
117 | + </div> | |
118 | + | |
119 | + </div> | |
120 | + </div> | |
121 | +</template> | |
122 | + | |
123 | +<script> | |
124 | +export default { | |
125 | + data() { | |
126 | + const today = new Date().toISOString().split('T')[0]; | |
127 | + return { | |
128 | + isReturned: true, | |
129 | + startDate: today, | |
130 | + startTime: "09:00", // 기본 시작 시간 09:00 | |
131 | + endDate: today, | |
132 | + endTime: "18:00", // 기본 종료 시간 18:00 | |
133 | + category: "", | |
134 | + dayCount: 1, | |
135 | + reason: "", // 사유 | |
136 | + listData: [ | |
137 | + { | |
138 | + type: '연차', | |
139 | + approvalType: '결재', | |
140 | + applicant: '홍길동', | |
141 | + period: '2025-05-10 ~ 2025-15-03', | |
142 | + requestDate: '2025-04-25', | |
143 | + status: '대기' | |
144 | + }, { | |
145 | + type: '반차', | |
146 | + approvalType: '전결', | |
147 | + applicant: '홍길동', | |
148 | + period: '2025-05-01 ~ 2025-05-03', | |
149 | + requestDate: '2025-04-25', | |
150 | + status: '승인' | |
151 | + }], | |
152 | + }; | |
153 | + }, | |
154 | + computed: { | |
155 | + // Pinia Store의 상태를 가져옵니다. | |
156 | + loginUser() { | |
157 | + const authStore = useAuthStore(); | |
158 | + return authStore.getLoginUser; | |
159 | + }, | |
160 | + }, | |
161 | + methods: { | |
162 | + // 폼 검증 메서드 | |
163 | + validateForm() { | |
164 | + // 필수 입력 필드 체크 | |
165 | + if ( | |
166 | + this.category && | |
167 | + this.startDate && | |
168 | + this.startTime && | |
169 | + this.endDate && | |
170 | + this.endTime && | |
171 | + this.dayCount > 0 && | |
172 | + this.reason.trim() !== "" | |
173 | + ) { | |
174 | + this.isFormValid = true; | |
175 | + } else { | |
176 | + this.isFormValid = false; | |
177 | + } | |
178 | + }, | |
179 | + calculateDayCount() { | |
180 | + const start = new Date(`${this.startDate}T${this.startTime}:00`); | |
181 | + const end = new Date(`${this.endDate}T${this.endTime}:00`); | |
182 | + | |
183 | + let totalHours = (end - start) / (1000 * 60 * 60); // 밀리초를 시간 단위로 변환 | |
184 | + | |
185 | + if (this.startDate !== this.endDate) { | |
186 | + // 시작일과 종료일이 다른경우 | |
187 | + const startDateObj = new Date(this.startDate); | |
188 | + const endDateObj = new Date(this.endDate); | |
189 | + const daysDifference = (endDateObj - startDateObj) / (1000 * 60 * 60 * 24); // 두 날짜 사이의 차이를 일수로 계산 | |
190 | + if (this.startTime !== "09:00" || this.endTime !== "18:00") { | |
191 | + this.dayCount = daysDifference + 0.5; // 시간 조건이 기준에서 벗어날 경우 | |
192 | + } else { | |
193 | + this.dayCount = Math.ceil(daysDifference + 1); // 시간 조건이 기준에 맞을 경우 | |
194 | + } | |
195 | + } else { | |
196 | + // 시작일과 종료일이 같은 경우 | |
197 | + if (this.startTime !== "09:00" || this.endTime !== "18:00") { | |
198 | + this.dayCount = 0.5; // 시작 시간 또는 종료 시간이 기준과 다를 경우 0.5 | |
199 | + } else { | |
200 | + this.dayCount = 1; // 기준 시간(09:00~18:00)이 맞으면 1일로 간주 | |
201 | + } | |
202 | + } | |
203 | + | |
204 | + this.validateForm(); // dayCount 변경 후 폼 재검증 | |
205 | + }, | |
206 | + handleSubmit() { | |
207 | + this.validateForm(); // 제출 시 유효성 확인 | |
208 | + if (this.isFormValid) { | |
209 | + localStorage.setItem('HyugaFormData', JSON.stringify(this.$data)); | |
210 | + alert("승인 요청이 완료되었습니다."); | |
211 | + // 추가 처리 로직 (API 요청 등) | |
212 | + } else { | |
213 | + alert("모든 필드를 올바르게 작성해주세요."); | |
214 | + } | |
215 | + }, | |
216 | + | |
217 | + | |
218 | + }, | |
219 | + mounted() { | |
220 | + // Load the saved form data when the page is loaded | |
221 | + this.loadFormData(); | |
222 | + }, | |
223 | + watch: { | |
224 | + startDate: 'calculateDayCount', | |
225 | + startTime: 'calculateDayCount', | |
226 | + endDate: 'calculateDayCount', | |
227 | + endTime: 'calculateDayCount', | |
228 | + reason: "validateForm", | |
229 | + category: 'category', | |
230 | + }, | |
231 | +}; | |
232 | +</script> |
+++ client/views/pages/Manager/attendance/ChuljangPumuiDetail.vue
... | ... | @@ -0,0 +1,203 @@ |
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 " :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" id="youremail" 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" id="yourPassword" 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" id="youremail" 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" id="yourPassword" 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" id="yourName" readonly> | |
50 | + </div> | |
51 | + <div class="col-12"> | |
52 | + <label for="yourName" class="form-label">출장목적</label> | |
53 | + <input v-model="name" type="text" name="name" class="form-control " id="yourName" 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 " id="yourName" readonly> | |
58 | + </div> | |
59 | + <div class="col-12 chuljang"> | |
60 | + <label for="yourName" class="form-label">내용</label> | |
61 | + <input v-model="name" type="text" name="name" class="form-control textarea " id="yourName" readonly> | |
62 | + </div> | |
63 | + <div class="col-12"> | |
64 | + <label for="yourName" class="form-label">법인카드</label> | |
65 | + <input v-model="name" type="text" name="name" class="form-control " id="yourName" readonly> | |
66 | + </div> | |
67 | + <div class="col-12"> | |
68 | + <label for="yourName" class="form-label">법인차량</label> | |
69 | + <input v-model="name" type="text" name="name" class="form-control " id="yourName" readonly> | |
70 | + </div> | |
71 | + <div class="col-12"> | |
72 | + <label for="yourName" class="form-label">품의 신청일</label> | |
73 | + <input v-model="name" type="text" name="name" class="form-control " id="yourName" readonly> | |
74 | + </div> | |
75 | + <div class="col-12 border-x" :class="{ return: isReturned }"> | |
76 | + <label for="yourName" class="form-label">반려사유</label> | |
77 | + <input v-model="name" type="text" name="name" class="form-control" id="yourName" readonly placeholder="2025-01-01"> | |
78 | + </div> | |
79 | + | |
80 | + | |
81 | + </form> | |
82 | + </div> | |
83 | + <div class="buttons"> | |
84 | + <button class="btn btn-red " type="submit">신청취소</button> | |
85 | + <button class="btn secondary" type="submit"> {{ isReturned ? '재신청' : '수정' }}</button> | |
86 | + <button class="btn primary" type="submit" v-if="!isReturned">복명서 작성</button> | |
87 | + <button class="btn tertiary " type="submit">목록</button> | |
88 | + </div> | |
89 | + | |
90 | + </div> | |
91 | + </div> | |
92 | +</template> | |
93 | + | |
94 | +<script> | |
95 | +export default { | |
96 | + data() { | |
97 | + const today = new Date().toISOString().split('T')[0]; | |
98 | + return { | |
99 | + isReturned: true, | |
100 | + startDate: today, | |
101 | + startTime: "09:00", // 기본 시작 시간 09:00 | |
102 | + endDate: today, | |
103 | + endTime: "18:00", // 기본 종료 시간 18:00 | |
104 | + category: "", | |
105 | + dayCount: 1, | |
106 | + reason: "", // 사유 | |
107 | + listData: [ | |
108 | + { | |
109 | + type: '연차', | |
110 | + approvalType: '결재', | |
111 | + applicant: '홍길동', | |
112 | + period: '2025-05-10 ~ 2025-15-03', | |
113 | + requestDate: '2025-04-25', | |
114 | + status: '대기' | |
115 | + }, { | |
116 | + type: '반차', | |
117 | + approvalType: '전결', | |
118 | + applicant: '홍길동', | |
119 | + period: '2025-05-01 ~ 2025-05-03', | |
120 | + requestDate: '2025-04-25', | |
121 | + status: '승인' | |
122 | + }], | |
123 | + }; | |
124 | + }, | |
125 | + computed: { | |
126 | + // Pinia Store의 상태를 가져옵니다. | |
127 | + loginUser() { | |
128 | + const authStore = useAuthStore(); | |
129 | + return authStore.getLoginUser; | |
130 | + }, | |
131 | + }, | |
132 | + methods: { | |
133 | + // 폼 검증 메서드 | |
134 | + validateForm() { | |
135 | + // 필수 입력 필드 체크 | |
136 | + if ( | |
137 | + this.category && | |
138 | + this.startDate && | |
139 | + this.startTime && | |
140 | + this.endDate && | |
141 | + this.endTime && | |
142 | + this.dayCount > 0 && | |
143 | + this.reason.trim() !== "" | |
144 | + ) { | |
145 | + this.isFormValid = true; | |
146 | + } else { | |
147 | + this.isFormValid = false; | |
148 | + } | |
149 | + }, | |
150 | + calculateDayCount() { | |
151 | + const start = new Date(`${this.startDate}T${this.startTime}:00`); | |
152 | + const end = new Date(`${this.endDate}T${this.endTime}:00`); | |
153 | + | |
154 | + let totalHours = (end - start) / (1000 * 60 * 60); // 밀리초를 시간 단위로 변환 | |
155 | + | |
156 | + if (this.startDate !== this.endDate) { | |
157 | + // 시작일과 종료일이 다른경우 | |
158 | + const startDateObj = new Date(this.startDate); | |
159 | + const endDateObj = new Date(this.endDate); | |
160 | + const daysDifference = (endDateObj - startDateObj) / (1000 * 60 * 60 * 24); // 두 날짜 사이의 차이를 일수로 계산 | |
161 | + if (this.startTime !== "09:00" || this.endTime !== "18:00") { | |
162 | + this.dayCount = daysDifference + 0.5; // 시간 조건이 기준에서 벗어날 경우 | |
163 | + } else { | |
164 | + this.dayCount = Math.ceil(daysDifference + 1); // 시간 조건이 기준에 맞을 경우 | |
165 | + } | |
166 | + } else { | |
167 | + // 시작일과 종료일이 같은 경우 | |
168 | + if (this.startTime !== "09:00" || this.endTime !== "18:00") { | |
169 | + this.dayCount = 0.5; // 시작 시간 또는 종료 시간이 기준과 다를 경우 0.5 | |
170 | + } else { | |
171 | + this.dayCount = 1; // 기준 시간(09:00~18:00)이 맞으면 1일로 간주 | |
172 | + } | |
173 | + } | |
174 | + | |
175 | + this.validateForm(); // dayCount 변경 후 폼 재검증 | |
176 | + }, | |
177 | + handleSubmit() { | |
178 | + this.validateForm(); // 제출 시 유효성 확인 | |
179 | + if (this.isFormValid) { | |
180 | + localStorage.setItem('HyugaFormData', JSON.stringify(this.$data)); | |
181 | + alert("승인 요청이 완료되었습니다."); | |
182 | + // 추가 처리 로직 (API 요청 등) | |
183 | + } else { | |
184 | + alert("모든 필드를 올바르게 작성해주세요."); | |
185 | + } | |
186 | + }, | |
187 | + | |
188 | + | |
189 | + }, | |
190 | + mounted() { | |
191 | + // Load the saved form data when the page is loaded | |
192 | + this.loadFormData(); | |
193 | + }, | |
194 | + watch: { | |
195 | + startDate: 'calculateDayCount', | |
196 | + startTime: 'calculateDayCount', | |
197 | + endDate: 'calculateDayCount', | |
198 | + endTime: 'calculateDayCount', | |
199 | + reason: "validateForm", | |
200 | + category: 'category', | |
201 | + }, | |
202 | +}; | |
203 | +</script> |
+++ client/views/pages/Manager/attendance/ChuljangStatue.vue
... | ... | @@ -0,0 +1,188 @@ |
1 | +<template> | |
2 | + <div class="col-lg-12"> | |
3 | + <div class="card"> | |
4 | + <div class="card-body"> | |
5 | + <h2 class="card-title">출장 현황</h2> | |
6 | + <!-- 폼그룹 --> | |
7 | + <div class="sch-form-wrap"> | |
8 | + <div class="input-group"> | |
9 | + <select name="" id="" class="form-select"> | |
10 | + <option value="">년도</option> | |
11 | + </select> | |
12 | + <select name="" id="" class="form-select"> | |
13 | + <option value="">월</option> | |
14 | + </select> | |
15 | + </div> | |
16 | + </div> | |
17 | + <!-- Table --> | |
18 | + <div class="tbl-wrap"> | |
19 | + <table id="myTable" class="tbl data"> | |
20 | + <!-- 동적으로 <th> 생성 --> | |
21 | + <thead> | |
22 | + <tr> | |
23 | + <th>출장구분 </th> | |
24 | + <th>출장지</th> | |
25 | + <th>목적</th> | |
26 | + <th>출장기간</th> | |
27 | + <th>품의서 상태</th> | |
28 | + <th>복명서 등록 여부</th> | |
29 | + <th>복명서 상태</th> | |
30 | + </tr> | |
31 | + </thead> | |
32 | + <!-- 동적으로 <td> 생성 --> | |
33 | + <tbody> | |
34 | + <tr v-for="(item, index) in listData" :key="index" :class="{ 'expired': isPastPeriod(item.period) }" > | |
35 | + <td>{{ item.type }}</td> | |
36 | + <td>{{ item.where }}</td> | |
37 | + <td>{{ item.purpose }}</td> | |
38 | + <td @click="goToPage('all')">{{ item.period }}</td> | |
39 | + <td :class="getStatusClass(item.pumuiStatue)" @click="goToPage('품의서')">{{ item.pumuiStatue }}</td> | |
40 | + <td :class="getBokmyeongClass(item.bokmyeong)" @click="goToPage('복명서')">{{ item.bokmyeong }}</td> | |
41 | + <td :class="getStatusClass(item.status)">{{ item.status }}</td> | |
42 | + </tr> | |
43 | + </tbody> | |
44 | + </table> | |
45 | + | |
46 | + </div> | |
47 | + <div class="pagination"> | |
48 | + <ul> | |
49 | + <!-- 왼쪽 화살표 (이전 페이지) --> | |
50 | + <li class="arrow" :class="{ disabled: currentPage === 1 }" @click="changePage(currentPage - 1)"> | |
51 | + < | |
52 | + </li> | |
53 | + | |
54 | + <!-- 페이지 번호 --> | |
55 | + <li v-for="page in totalPages" :key="page" :class="{ active: currentPage === page }" | |
56 | + @click="changePage(page)"> | |
57 | + {{ page }} | |
58 | + </li> | |
59 | + | |
60 | + <!-- 오른쪽 화살표 (다음 페이지) --> | |
61 | + <li class="arrow" :class="{ disabled: currentPage === totalPages }" @click="changePage(currentPage + 1)"> | |
62 | + > | |
63 | + </li> | |
64 | + </ul> | |
65 | + </div> | |
66 | + | |
67 | + </div> | |
68 | + </div> | |
69 | + </div> | |
70 | +</template> | |
71 | + | |
72 | +<script> | |
73 | +import { ref } from 'vue'; | |
74 | +import { SearchOutlined } from '@ant-design/icons-vue'; | |
75 | +export default { | |
76 | + data() { | |
77 | + return { | |
78 | + showOptions: false, | |
79 | + currentPage: 1, | |
80 | + totalPages: 3, | |
81 | + photoicon: "/client/resources/img/photo_icon.png", | |
82 | + // 데이터 초기화 | |
83 | + years: [2023, 2024, 2025], // 연도 목록 | |
84 | + months: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], // 월 목록 | |
85 | + selectedYear: '', | |
86 | + selectedMonth: '', | |
87 | + listData: [ | |
88 | + { | |
89 | + type: '연차', | |
90 | + where: '상주시청', | |
91 | + purpose: '유지보수', | |
92 | + period: '2025-05-10 ~ 2025-05-23', | |
93 | + pumuiStatue: '대기', | |
94 | + bokmyeong: '미등록', | |
95 | + status: '대기' | |
96 | + }, { | |
97 | + type: '연차', | |
98 | + where: '상주시청', | |
99 | + purpose: '유지보수', | |
100 | + period: '2025-05-10 ~ 2025-05-10', | |
101 | + pumuiStatue: '승인', | |
102 | + bokmyeong: '등록', | |
103 | + status: '대기' | |
104 | + },], | |
105 | + filteredData: [], | |
106 | + }; | |
107 | + }, | |
108 | + components: { | |
109 | + SearchOutlined | |
110 | + }, | |
111 | + computed: { | |
112 | + }, | |
113 | + methods: { | |
114 | + goToAttendancePage(item) { | |
115 | + this.$router.push({ name: 'AttendanceDetail', query: { id: item.id } }); | |
116 | + }, | |
117 | + changePage(page) { | |
118 | + if (page < 1 || page > this.totalPages) return; | |
119 | + this.currentPage = page; | |
120 | + this.$emit('page-changed', page); // 필요 시 부모에 알림 | |
121 | + }, | |
122 | + async onClickSubmit() { | |
123 | + // `useMutation` 훅을 사용하여 mutation 함수 가져오기 | |
124 | + const { mutate, onDone, onError } = useMutation(mygql); | |
125 | + | |
126 | + try { | |
127 | + const result = await mutate(); | |
128 | + console.log(result); | |
129 | + } catch (error) { | |
130 | + console.error('Mutation error:', error); | |
131 | + } | |
132 | + }, | |
133 | + goToPage(type) { | |
134 | + if (type === '품의서') { | |
135 | + this.$router.push('/ChuljangPumuiDetail.page'); | |
136 | + } else if (type === 'all') { | |
137 | + this.$router.push('/ChuljangDetailAll.page'); | |
138 | + } else if (type === '복명서') { | |
139 | + this.$router.push('/ChuljangBokmyeongDetail.page'); | |
140 | + } | |
141 | +}, | |
142 | + | |
143 | + // 상태에 따른 클래스 반환 메소드 | |
144 | + getStatusClass(status) { | |
145 | + return status === 'active' ? 'status-active' : 'status-inactive'; | |
146 | + }, | |
147 | + getStatusClass(status, pumuiStatue) { | |
148 | + // Check the 'status' and 'pumuiStatue' to return the correct class | |
149 | + if (pumuiStatue === '승인') return 'status-approved'; | |
150 | + if (pumuiStatue === '대기') return 'status-pending'; | |
151 | + // If no match, fallback to status-based class | |
152 | + if (status === '대기') return 'status-pending'; | |
153 | + if (status === '승인') return 'status-approved'; | |
154 | + | |
155 | + // Default empty string | |
156 | + return ''; | |
157 | +}, | |
158 | +getBokmyeongClass(bokmyeong) { | |
159 | + if (bokmyeong === '등록') return 'status-approved'; | |
160 | + if (bokmyeong === '미등록') return 'status-pending'; | |
161 | + return ''; | |
162 | +}, | |
163 | + isPastPeriod(period) { | |
164 | + // 예: '2025-05-01 ~ 2025-05-03' → 종료일 추출 | |
165 | + const endDateStr = period.split('~')[1]?.trim(); | |
166 | + if (!endDateStr) return false; | |
167 | + | |
168 | + const endDate = new Date(endDateStr); | |
169 | + const today = new Date(); | |
170 | + | |
171 | + // 현재 날짜보다 과거면 true | |
172 | + return endDate < today; | |
173 | + } | |
174 | + }, | |
175 | + created() { | |
176 | + }, | |
177 | + mounted() { | |
178 | + | |
179 | + | |
180 | + }, | |
181 | +}; | |
182 | +</script> | |
183 | + | |
184 | +<style scoped> | |
185 | +tr { | |
186 | + cursor: pointer; | |
187 | +} | |
188 | +</style> |
--- client/views/pages/Manager/attendance/attendance.vue
+++ client/views/pages/Manager/attendance/attendance.vue
... | ... | @@ -1,132 +1,178 @@ |
1 | 1 |
<template> |
2 |
- <div class="pagetitle"> |
|
3 |
- <h2>직원관리</h2> |
|
2 |
+ <div class="sidemenu"> |
|
3 |
+ <div class="myinfo simple"> |
|
4 |
+ <div class="name-box"> |
|
5 |
+ <div class="img-area"> |
|
6 |
+ <div><img :src="photoicon" alt=""> |
|
7 |
+ <p class="name">OOO과장</p> |
|
8 |
+ </div> |
|
9 |
+ <div class="info"> |
|
10 |
+ <p>솔루션 개발팀</p> |
|
11 |
+ <i class="fa-bars"></i> |
|
12 |
+ <p>팀장</p> |
|
13 |
+ </div> |
|
14 |
+ </div> |
|
15 |
+ </div> |
|
16 |
+ <details class="menu-box"> |
|
17 |
+ <summary><p>근태현황</p><div class="icon"><img :src="topmenuicon" alt=""></div></summary> |
|
18 |
+ <ul> |
|
19 |
+ <li> <router-link to="/myAttendance.page" exact-active-class="active-link" v-slot="{ isExactActive }"> |
|
20 |
+ <p>나의 근태현황</p> |
|
21 |
+ <div class="icon" v-if="isExactActive"> |
|
22 |
+ <img :src="menuicon" alt=""> |
|
23 |
+ </div> |
|
24 |
+ </router-link></li> |
|
25 |
+ <li> |
|
26 |
+ <router-link to="/buseoAttendance.page" exact-active-class="active-link" v-slot="{ isExactActive }"> |
|
27 |
+ <p>부서별 근태현황</p> |
|
28 |
+ <div class="icon" v-if="isExactActive"> |
|
29 |
+ <img :src="menuicon" alt=""> |
|
30 |
+ </div> |
|
31 |
+ </router-link> |
|
32 |
+ </li> |
|
33 |
+ </ul> |
|
34 |
+ </details> |
|
35 |
+ <details class="menu-box"> |
|
36 |
+ <summary><p>휴가</p><div class="icon"><img :src="topmenuicon" alt=""></div></summary> |
|
37 |
+ <ul> |
|
38 |
+ <li> <router-link to="/hyugaStatue.page" exact-active-class="active-link" v-slot="{ isExactActive }"> |
|
39 |
+ <p>휴가 현황</p> |
|
40 |
+ <div class="icon" v-if="isExactActive"> |
|
41 |
+ <img :src="menuicon" alt=""> |
|
42 |
+ </div> |
|
43 |
+ </router-link></li> |
|
44 |
+ <li> |
|
45 |
+ <router-link to="/hyugaInsert.page" exact-active-class="active-link" v-slot="{ isExactActive }"> |
|
46 |
+ <p>휴가 신청</p> |
|
47 |
+ <div class="icon" v-if="isExactActive"> |
|
48 |
+ <img :src="menuicon" alt=""> |
|
49 |
+ </div> |
|
50 |
+ </router-link> |
|
51 |
+ </li> |
|
52 |
+ </ul> |
|
53 |
+ </details> |
|
54 |
+ <details class="menu-box"> |
|
55 |
+ <summary><p>출장</p><div class="icon"><img :src="topmenuicon" alt=""></div></summary> |
|
56 |
+ <ul> |
|
57 |
+ <li> <router-link to="/ChuljangStatue.page" exact-active-class="active-link" v-slot="{ isExactActive }"> |
|
58 |
+ <p>출장 현황</p> |
|
59 |
+ <div class="icon" v-if="isExactActive"> |
|
60 |
+ <img :src="menuicon" alt=""> |
|
61 |
+ </div> |
|
62 |
+ </router-link></li> |
|
63 |
+ <li> |
|
64 |
+ <router-link to="/ChuljangInsert.page" exact-active-class="active-link" v-slot="{ isExactActive }"> |
|
65 |
+ <p>출장 신청</p> |
|
66 |
+ <div class="icon" v-if="isExactActive"> |
|
67 |
+ <img :src="menuicon" alt=""> |
|
68 |
+ </div> |
|
69 |
+ </router-link> |
|
70 |
+ </li> |
|
71 |
+ </ul> |
|
72 |
+ </details> |
|
73 |
+ </div> |
|
4 | 74 |
</div> |
5 | 75 |
<!-- End Page Title --> |
6 |
- <section class="section"> |
|
7 |
- <div class="row"> |
|
76 |
+ <div class="content"> |
|
77 |
+ <router-view></router-view> |
|
8 | 78 |
|
9 |
- |
|
10 |
- <div class="col-lg-12"> |
|
11 |
- <div class="card"> |
|
12 |
- <div class="card-body"> |
|
13 |
- <h5 class="card-title">직원관리</h5> |
|
14 |
- <div class="d-flex pb-3 justify-content-between"> |
|
15 |
- <div class="datatable-search d-flex gap-1 "> |
|
16 |
- <div class=""> |
|
17 |
- <select class="form-select " v-model="selectedDept"> |
|
18 |
- <option value="" >이름</option> |
|
19 |
- </select> |
|
20 |
- </div> |
|
21 |
- <div class="search-bar d-flex gap-2"> |
|
22 |
- <form class="search-form d-flex align-items-center" method="POST" action="#" |
|
23 |
- @submit.prevent="updateMember"> |
|
24 |
- <input type="text" v-model="searchQuery" name="query" placeholder="Search" title="Enter search keyword"> |
|
25 |
- <button type="submit" title="Search"><i class="bi bi-search"></i></button> |
|
26 |
- </form> |
|
27 |
- </div> |
|
28 |
- <button type="button" class="btn btn-outline-secondary" |
|
29 |
- @click="filterData">조회</button> |
|
30 |
- |
|
31 |
- </div> |
|
32 |
- <div class="d-flex justify-content-end "> |
|
33 |
- <!-- <button type="button" class="btn btn-outline-secondary" @click="registerLeave"> |
|
34 |
- 등록 |
|
35 |
- </button> |
|
36 |
- <button type="button" class="btn btn-outline-success" @click="saveChanges"> |
|
37 |
- 저장 |
|
38 |
- </button> --> |
|
39 |
- <button type="button" class="btn btn-outline-danger" @click="deletePending"> |
|
40 |
- 삭제 |
|
41 |
- </button> |
|
42 |
- </div> |
|
43 |
- |
|
44 |
- </div> |
|
45 |
- <!-- Table --> |
|
46 |
- <table id="myTable" class="table datatable table-hover"> |
|
47 |
- <!-- 동적으로 <th> 생성 --> |
|
48 |
- <thead> |
|
49 |
- <tr> |
|
50 |
- <th>No </th> |
|
51 |
- <th>이름</th> |
|
52 |
- <th>부서</th> |
|
53 |
- <th>직급</th> |
|
54 |
- <th>이메일</th> |
|
55 |
- </tr> |
|
56 |
- </thead> |
|
57 |
- <!-- 동적으로 <td> 생성 --> |
|
58 |
- <tbody> |
|
59 |
- <tr v-for="(item, index) in UserInfo" :key="index"> |
|
60 |
- <td> |
|
61 |
- <div class="form-check"> |
|
62 |
- <label class="form-check-label" for="acceptTerms">{{ index + 1 }}</label> |
|
63 |
- <input v-model="item.acceptTerms" class="form-check-input" type="checkbox" /> |
|
64 |
- </div> |
|
65 |
- </td> |
|
66 |
- <td>{{ item.name }}</td> <!-- 이름 --> |
|
67 |
- <td>{{ item.dept }}</td> <!-- 부서 --> |
|
68 |
- <td>{{ item.level }}</td> <!-- 레벨 --> |
|
69 |
- <td>{{ item.email }}</td> <!-- 이메일 --> |
|
70 |
- </tr> |
|
71 |
- </tbody> |
|
72 |
- </table> |
|
73 |
- |
|
74 |
- <!-- End Table --> |
|
75 |
- </div> |
|
76 |
- </div> |
|
77 |
- </div> |
|
78 |
- </div> |
|
79 |
- </section> |
|
79 |
+</div> |
|
80 | 80 |
</template> |
81 | 81 |
|
82 | 82 |
<script> |
83 |
+import { ref } from 'vue'; |
|
84 |
+ |
|
83 | 85 |
export default { |
84 | 86 |
data() { |
85 | 87 |
return { |
88 |
+ photoicon: "/client/resources/img/photo_icon.png", |
|
89 |
+ menuicon: "/client/resources/img/menuicon.png", |
|
90 |
+ topmenuicon: "/client/resources/img/topmenuicon.png", |
|
86 | 91 |
// 데이터 초기화 |
87 |
- levels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], // 월 목록 |
|
88 |
- selectedDept: '', |
|
89 |
- selectedlevel: '', |
|
92 |
+ years: [2023, 2024, 2025], // 연도 목록 |
|
93 |
+ months: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], // 월 목록 |
|
94 |
+ selectedYear: '', |
|
95 |
+ selectedMonth: '', |
|
96 |
+ DeptData: [ |
|
97 |
+ { member: '', deptNM: '', acceptTerms: false }, |
|
98 |
+ // 더 많은 데이터 추가... |
|
99 |
+ ], |
|
90 | 100 |
filteredData: [], |
91 | 101 |
}; |
92 | 102 |
}, |
93 | 103 |
computed: { |
94 |
- |
|
95 | 104 |
}, |
96 | 105 |
methods: { |
97 |
- |
|
98 |
- |
|
99 |
- deletePending() { |
|
100 |
- // 선택된 항목만 필터링하여 삭제 |
|
101 |
- const selectedItems = this.UserInfoData.filter(item => item.acceptTerms); |
|
106 |
+ async onClickSubmit() { |
|
107 |
+ // `useMutation` 훅을 사용하여 mutation 함수 가져오기 |
|
108 |
+ const { mutate, onDone, onError } = useMutation(mygql); |
|
102 | 109 |
|
103 |
- // 승인된 항목이 없으면 삭제 진행 |
|
104 |
- if (selectedItems.length > 0) { |
|
105 |
- this.UserInfoData = this.UserInfoData.filter(item => !item.acceptTerms); |
|
106 |
- alert(`${selectedItems.length}개의 항목이 삭제되었습니다.`); |
|
107 |
- } else { |
|
108 |
- alert("선택된 항목이 없습니다."); |
|
109 |
- } |
|
110 |
- }, |
|
111 |
- |
|
110 |
+ try { |
|
111 |
+ const result = await mutate(); |
|
112 |
+ console.log(result); |
|
113 |
+ } catch (error) { |
|
114 |
+ console.error('Mutation error:', error); |
|
115 |
+ } |
|
116 |
+ }, |
|
117 |
+ registerLeave() { |
|
118 |
+ console.log("등록 버튼 클릭됨"); |
|
119 |
+ |
|
120 |
+ // Vue의 반응성 문제를 피하기 위해, 새로운 객체를 추가합니다. |
|
121 |
+ this.DeptData = [ |
|
122 |
+ ...this.DeptData, |
|
123 |
+ { member: '', deptNM: '', acceptTerms: false } |
|
124 |
+ ]; |
|
125 |
+ |
|
126 |
+ console.log(this.DeptData); // 배열 상태 출력 |
|
127 |
+ }, |
|
128 |
+ saveChanges() { |
|
129 |
+ // 로컬스토리지에 DeptData 저장 |
|
130 |
+ localStorage.setItem('DeptData', JSON.stringify(this.DeptData)); |
|
131 |
+ console.log('데이터가 로컬스토리지에 저장되었습니다.'); |
|
132 |
+ }, |
|
133 |
+ deletePending() { |
|
134 |
+ // 선택된 항목만 필터링하여 삭제 |
|
135 |
+ const selectedItems = this.DeptData.filter(item => item.acceptTerms); |
|
136 |
+ |
|
137 |
+ // 승인된 항목이 없으면 삭제 진행 |
|
138 |
+ if (selectedItems.length > 0) { |
|
139 |
+ this.DeptData = this.DeptData.filter(item => !item.acceptTerms); |
|
140 |
+ alert(`${selectedItems.length}개의 항목이 삭제되었습니다.`); |
|
141 |
+ } else { |
|
142 |
+ alert("선택된 항목이 없습니다."); |
|
143 |
+ } |
|
144 |
+ }, |
|
145 |
+ // 날짜 필터 적용 |
|
146 |
+ filterData() { |
|
147 |
+ this.filteredData = this.DeptData.filter(item => { |
|
148 |
+ const itemYear = new Date(item.startDate).getFullYear(); |
|
149 |
+ const itemMonth = new Date(item.startDate).getMonth() + 1; // 월은 0부터 시작하므로 1을 더해줍니다. |
|
150 |
+ |
|
151 |
+ return ( |
|
152 |
+ (!this.selectedYear || itemYear === parseInt(this.selectedYear)) && |
|
153 |
+ (!this.selectedMonth || itemMonth === parseInt(this.selectedMonth)) |
|
154 |
+ ); |
|
155 |
+ }); |
|
156 |
+ }, |
|
157 |
+ |
|
112 | 158 |
// 페이지 변경 |
113 | 159 |
changePage(page) { |
114 | 160 |
this.currentPage = page; |
115 | 161 |
}, |
116 | 162 |
}, |
117 | 163 |
created() { |
118 |
- // 로컬스토리지에서 UserInfoData 불러오기 |
|
119 |
- const storedUserInfo = localStorage.getItem('UserInfo'); |
|
120 |
- console.log(storedUserInfo); |
|
121 |
- if (storedUserInfo) { |
|
122 |
- // 로컬스토리지에서 데이터를 가져와 UserInfoData에 설정 |
|
123 |
- const parsedData = JSON.parse(storedUserInfo); |
|
124 |
- this.UserInfo = Array.isArray(parsedData) ? parsedData : [parsedData]; |
|
125 |
- } |
|
164 |
+ // 로컬스토리지에서 기존 데이터가 있으면 불러오기 |
|
165 |
+ const storedData = localStorage.getItem('DeptData'); |
|
166 |
+ console.log(storedData); |
|
167 |
+ if (storedData) { |
|
168 |
+ this.DeptData = JSON.parse(storedData); |
|
169 |
+ } |
|
126 | 170 |
}, |
127 | 171 |
mounted() { |
128 |
- |
|
129 |
- |
|
172 |
+ |
|
173 |
+ // 처음에는 모든 데이터를 표시 |
|
174 |
+ this.filteredData = this.DeptData; |
|
175 |
+ |
|
130 | 176 |
}, |
131 | 177 |
}; |
132 | 178 |
</script> |
+++ client/views/pages/Manager/attendance/buseoAttendance.vue
... | ... | @@ -0,0 +1,208 @@ |
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 name="" id="" class="form-select"> | |
9 | + <option value="">년도</option> | |
10 | + </select> | |
11 | + <select name="" id="" class="form-select"> | |
12 | + <option value="">월</option> | |
13 | + </select> | |
14 | + <select name="" id="" class="form-select"> | |
15 | + <option value="">구분</option> | |
16 | + </select> | |
17 | + <div class="sch-input"> | |
18 | + <input type="text" class="form-control"> | |
19 | + <button class="ico-sch"><SearchOutlined /></button> | |
20 | + </div> | |
21 | + </div> | |
22 | + </div> | |
23 | + | |
24 | + <!-- Table --> | |
25 | + <div class="tbl-wrap"> | |
26 | + <table id="myTable" class="tbl data buseo"> | |
27 | + <!-- 동적으로 <th> 생성 --> | |
28 | + <thead> | |
29 | + <tr> | |
30 | + <th>부서 </th> | |
31 | + <th>직급</th> | |
32 | + <th>이름</th> | |
33 | + <th>지각</th> | |
34 | + <th>조기퇴근</th> | |
35 | + <th>결근</th> | |
36 | + <th>출장</th> | |
37 | + <th>주말출근</th> | |
38 | + <th>대체휴가</th> | |
39 | + <th>휴가</th> | |
40 | + <th>공가</th> | |
41 | + <th>병가</th> | |
42 | + </tr> | |
43 | + </thead> | |
44 | + <!-- 동적으로 <td> 생성 --> | |
45 | + <tbody> | |
46 | + <tr v-for="(item, index) in listData" :key="index" @click="goToAttendancePage(item)"> | |
47 | + <td>{{ item.department }}</td> | |
48 | + <td>{{ item.position }}</td> | |
49 | + <td>{{ item.name }}</td> | |
50 | + <td>{{ item.late }}</td> | |
51 | + <td>{{ item.earlyLeave }}</td> | |
52 | + <td>{{ item.absence }}</td> | |
53 | + <td>{{ item.businessTrip }}</td> | |
54 | + <td>{{ item.weekendWork }}</td> | |
55 | + <td>{{ item.substituteHoliday }}</td> | |
56 | + <td>{{ item.vacation }}</td> | |
57 | + <td>{{ item.publicHoliday }}</td> | |
58 | + <td>{{ item.sickLeave }}</td> | |
59 | + </tr> | |
60 | + </tbody> | |
61 | + </table> | |
62 | + | |
63 | + </div> | |
64 | + <div class="pagination"> | |
65 | + <ul> | |
66 | + <!-- 왼쪽 화살표 (이전 페이지) --> | |
67 | + <li | |
68 | + class="arrow" | |
69 | + :class="{ disabled: currentPage === 1 }" | |
70 | + @click="changePage(currentPage - 1)" | |
71 | + > | |
72 | + < | |
73 | + </li> | |
74 | + | |
75 | + <!-- 페이지 번호 --> | |
76 | + <li | |
77 | + v-for="page in totalPages" | |
78 | + :key="page" | |
79 | + :class="{ active: currentPage === page }" | |
80 | + @click="changePage(page)" | |
81 | + > | |
82 | + {{ page }} | |
83 | + </li> | |
84 | + | |
85 | + <!-- 오른쪽 화살표 (다음 페이지) --> | |
86 | + <li | |
87 | + class="arrow" | |
88 | + :class="{ disabled: currentPage === totalPages }" | |
89 | + @click="changePage(currentPage + 1)" | |
90 | + > | |
91 | + > | |
92 | + </li> | |
93 | + </ul> | |
94 | + </div> | |
95 | + | |
96 | + </div> | |
97 | + </div> | |
98 | +</div> | |
99 | +</template> | |
100 | + | |
101 | +<script> | |
102 | +import { ref } from 'vue'; | |
103 | +import { SearchOutlined } from '@ant-design/icons-vue'; | |
104 | +export default { | |
105 | + data() { | |
106 | + return { | |
107 | + showOptions: false, | |
108 | + currentPage: 1, | |
109 | + totalPages: 3, | |
110 | + photoicon: "/client/resources/img/photo_icon.png", | |
111 | + // 데이터 초기화 | |
112 | + years: [2023, 2024, 2025], // 연도 목록 | |
113 | + months: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], // 월 목록 | |
114 | + selectedYear: '', | |
115 | + selectedMonth: '', | |
116 | + listData: [ | |
117 | + { | |
118 | + department: '인사팀', | |
119 | + position: '팀장', | |
120 | + name: '홍길동', | |
121 | + late: 2, | |
122 | + earlyLeave: 1, | |
123 | + absence: 0, | |
124 | + businessTrip: 1, | |
125 | + weekendWork: 0, | |
126 | + substituteHoliday: 1, | |
127 | + vacation: 5, | |
128 | + publicHoliday: 0, | |
129 | + sickLeave: 1, | |
130 | + }, | |
131 | + { | |
132 | + department: '개발팀', | |
133 | + position: '사원', | |
134 | + name: '김철수', | |
135 | + late: 1, | |
136 | + earlyLeave: 0, | |
137 | + absence: 1, | |
138 | + businessTrip: 0, | |
139 | + weekendWork: 2, | |
140 | + substituteHoliday: 0, | |
141 | + vacation: 3, | |
142 | + publicHoliday: 1, | |
143 | + sickLeave: 0, | |
144 | + }, | |
145 | + // 추가 데이터... | |
146 | + ], | |
147 | + filteredData: [], | |
148 | + }; | |
149 | + }, | |
150 | + components:{ | |
151 | + SearchOutlined | |
152 | + }, | |
153 | + computed: { | |
154 | + }, | |
155 | + methods: { | |
156 | + goToAttendancePage(item) { | |
157 | + this.$router.push({ name: 'AttendanceDetail', query: { id: item.id } }); | |
158 | + }, | |
159 | + changePage(page) { | |
160 | + if (page < 1 || page > this.totalPages) return; | |
161 | + this.currentPage = page; | |
162 | + this.$emit('page-changed', page); // 필요 시 부모에 알림 | |
163 | + }, | |
164 | + async onClickSubmit() { | |
165 | + // `useMutation` 훅을 사용하여 mutation 함수 가져오기 | |
166 | + const { mutate, onDone, onError } = useMutation(mygql); | |
167 | + | |
168 | + try { | |
169 | + const result = await mutate(); | |
170 | + console.log(result); | |
171 | + } catch (error) { | |
172 | + console.error('Mutation error:', error); | |
173 | + } | |
174 | + }, | |
175 | + goToPage(type) { | |
176 | + if (type === '휴가') { | |
177 | + this.$router.push('/HyugaDetail.page'); | |
178 | + } else if (type === '출장') { | |
179 | + this.$router.push('/ChuljangDetail.page'); | |
180 | + } | |
181 | +}, | |
182 | + getStatusClass(status) { | |
183 | + if (status === '승인') return 'status-approved'; | |
184 | + if (status === '대기') return 'status-pending'; | |
185 | + return ''; | |
186 | + }, | |
187 | + isPastPeriod(period) { | |
188 | + // 예: '2025-05-01 ~ 2025-05-03' → 종료일 추출 | |
189 | + const endDateStr = period.split('~')[1]?.trim(); | |
190 | + if (!endDateStr) return false; | |
191 | + | |
192 | + const endDate = new Date(endDateStr); | |
193 | + const today = new Date(); | |
194 | + | |
195 | + // 현재 날짜보다 과거면 true | |
196 | + return endDate < today; | |
197 | + } | |
198 | + }, | |
199 | + created() { | |
200 | + }, | |
201 | + mounted() { | |
202 | + | |
203 | + | |
204 | + }, | |
205 | +}; | |
206 | +</script> | |
207 | + | |
208 | +<style scoped></style> |
+++ client/views/pages/Manager/attendance/hyugaStatue.vue
... | ... | @@ -0,0 +1,205 @@ |
1 | +<template> | |
2 | +<div class="col-lg-12"> | |
3 | + <div class="card"> | |
4 | + <div class="card-body"> | |
5 | + <h2 class="card-title">휴가 현황</h2> | |
6 | + <!-- 폼그룹 --> | |
7 | +<div class="form-group"> | |
8 | + <div class="form-conts"> | |
9 | + <div class="form-conts datepicker-conts"> | |
10 | + <div class="datepicker-input"> | |
11 | + <input type="date" class="form-control datepicker cal" placeholder="YYYY.MM.DD" id="cal" style="max-width: 200px;"> | |
12 | + <mark>~</mark> | |
13 | + <input type="date" class="form-control datepicker cal" placeholder="YYYY.MM.DD" id="cal" style="max-width: 200px;"> | |
14 | + </div> | |
15 | + </div> | |
16 | + </div> | |
17 | +</div> | |
18 | +<!-- //폼그룹 --> | |
19 | + <div class="boxs"> | |
20 | + <div class="color-boxs"> | |
21 | + <div class="box "> | |
22 | + <h3>지각</h3> | |
23 | + 3 | |
24 | + </div> | |
25 | + <div class="box blue"> | |
26 | + <h3>조기퇴근</h3> | |
27 | + 3 | |
28 | + </div> | |
29 | + <div class="box red"> | |
30 | + <h3>결근</h3> | |
31 | + 3 | |
32 | + </div> | |
33 | + <div class="box green"> | |
34 | + <h3>결근</h3> | |
35 | + 3 | |
36 | + </div> | |
37 | + <div class="box purple"> | |
38 | + <h3>결근</h3> | |
39 | + 3 | |
40 | + </div> | |
41 | + <div class="box orange"> | |
42 | + <h3>결근</h3> | |
43 | + 3 | |
44 | + </div> | |
45 | + </div> | |
46 | + </div> | |
47 | + <!-- Table --> | |
48 | + <div class="tbl-wrap"> | |
49 | + <table id="myTable" class="tbl data"> | |
50 | + <!-- 동적으로 <th> 생성 --> | |
51 | + <thead> | |
52 | + <tr> | |
53 | + <th>구분 </th> | |
54 | + <th>기간</th> | |
55 | + <th>승인자</th> | |
56 | + <th>신청일</th> | |
57 | + <th>상태</th> | |
58 | + </tr> | |
59 | + </thead> | |
60 | + <!-- 동적으로 <td> 생성 --> | |
61 | + <tbody> | |
62 | + <tr v-for="(item, index) in listData" :key="index" @click="goToDetailPage(item)"> | |
63 | + <td>{{ item.type }}</td> | |
64 | + <td>{{ item.period }}</td> | |
65 | + <td>{{ item.approval }}</td> | |
66 | + <td>{{ item.requestDate }}</td> | |
67 | + <td :class="getStatusClass(item.status)">{{ item.status }}</td> | |
68 | + </tr> | |
69 | + </tbody> | |
70 | + </table> | |
71 | + | |
72 | + </div> | |
73 | + <div class="pagination"> | |
74 | + <ul> | |
75 | + <!-- 왼쪽 화살표 (이전 페이지) --> | |
76 | + <li | |
77 | + class="arrow" | |
78 | + :class="{ disabled: currentPage === 1 }" | |
79 | + @click="changePage(currentPage - 1)" | |
80 | + > | |
81 | + < | |
82 | + </li> | |
83 | + | |
84 | + <!-- 페이지 번호 --> | |
85 | + <li | |
86 | + v-for="page in totalPages" | |
87 | + :key="page" | |
88 | + :class="{ active: currentPage === page }" | |
89 | + @click="changePage(page)" | |
90 | + > | |
91 | + {{ page }} | |
92 | + </li> | |
93 | + | |
94 | + <!-- 오른쪽 화살표 (다음 페이지) --> | |
95 | + <li | |
96 | + class="arrow" | |
97 | + :class="{ disabled: currentPage === totalPages }" | |
98 | + @click="changePage(currentPage + 1)" | |
99 | + > | |
100 | + > | |
101 | + </li> | |
102 | + </ul> | |
103 | + </div> | |
104 | + | |
105 | + </div> | |
106 | + </div> | |
107 | +</div> | |
108 | +</template> | |
109 | + | |
110 | +<script> | |
111 | +import { ref } from 'vue'; | |
112 | +import { SearchOutlined } from '@ant-design/icons-vue'; | |
113 | +export default { | |
114 | + data() { | |
115 | + return { | |
116 | + showOptions: false, | |
117 | + currentPage: 1, | |
118 | + totalPages: 3, | |
119 | + photoicon: "/client/resources/img/photo_icon.png", | |
120 | + // 데이터 초기화 | |
121 | + years: [2023, 2024, 2025], // 연도 목록 | |
122 | + months: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], // 월 목록 | |
123 | + selectedYear: '', | |
124 | + selectedMonth: '', | |
125 | + listData: [ | |
126 | + { | |
127 | + type: '연차', | |
128 | + period: '2025-05-10 ~ 2025-15-03', | |
129 | + approval: '홍길동', | |
130 | + requestDate: '2025-04-25', | |
131 | + status: '대기' | |
132 | + }, { | |
133 | + type: '반차', | |
134 | + period: '2025-05-01 ~ 2025-05-03', | |
135 | + approval: '홍길동', | |
136 | + requestDate: '2025-04-25', | |
137 | + status: '승인' | |
138 | + }], | |
139 | + filteredData: [], | |
140 | + }; | |
141 | + }, | |
142 | + components:{ | |
143 | + SearchOutlined | |
144 | + }, | |
145 | + computed: { | |
146 | + }, | |
147 | + methods: { | |
148 | + | |
149 | + goToAttendancePage(item) { | |
150 | + this.$router.push({ name: 'AttendanceDetail', query: { id: item.id } }); | |
151 | + }, | |
152 | + changePage(page) { | |
153 | + if (page < 1 || page > this.totalPages) return; | |
154 | + this.currentPage = page; | |
155 | + this.$emit('page-changed', page); // 필요 시 부모에 알림 | |
156 | + }, | |
157 | + async onClickSubmit() { | |
158 | + // `useMutation` 훅을 사용하여 mutation 함수 가져오기 | |
159 | + const { mutate, onDone, onError } = useMutation(mygql); | |
160 | + | |
161 | + try { | |
162 | + const result = await mutate(); | |
163 | + console.log(result); | |
164 | + } catch (error) { | |
165 | + console.error('Mutation error:', error); | |
166 | + } | |
167 | + }, | |
168 | + goToDetailPage(item) { | |
169 | + // item.id 또는 다른 식별자를 사용하여 URL을 구성할 수 있습니다. | |
170 | + this.$router.push({ path: `/HyugaDetail.page`, query: { id: item.id } }); | |
171 | + }, | |
172 | + | |
173 | + // 상태에 따른 클래스 반환 메소드 | |
174 | + getStatusClass(status) { | |
175 | + return status === 'active' ? 'status-active' : 'status-inactive'; | |
176 | + }, | |
177 | + getStatusClass(status) { | |
178 | + if (status === '승인') return 'status-approved'; | |
179 | + if (status === '대기') return 'status-pending'; | |
180 | + return ''; | |
181 | + }, | |
182 | + isPastPeriod(period) { | |
183 | + // 예: '2025-05-01 ~ 2025-05-03' → 종료일 추출 | |
184 | + const endDateStr = period.split('~')[1]?.trim(); | |
185 | + if (!endDateStr) return false; | |
186 | + | |
187 | + const endDate = new Date(endDateStr); | |
188 | + const today = new Date(); | |
189 | + | |
190 | + // 현재 날짜보다 과거면 true | |
191 | + return endDate < today; | |
192 | + } | |
193 | + }, | |
194 | + created() { | |
195 | + }, | |
196 | + mounted() { | |
197 | + | |
198 | + | |
199 | + }, | |
200 | +}; | |
201 | +</script> | |
202 | + | |
203 | +<style scoped> | |
204 | +tr{cursor: pointer;} | |
205 | +</style> |
+++ client/views/pages/Manager/attendance/myAttendance.vue
... | ... | @@ -0,0 +1,248 @@ |
1 | +<template> | |
2 | +<div class="card "> | |
3 | + <div class="card-body"> | |
4 | + <h2 class="card-title">나의 근태현황</h2> | |
5 | + <div class="sch-wrap"> | |
6 | + <div class="sch-form-wrap title-wrap"> | |
7 | + <div class="flex"> | |
8 | + <div class="sub flex"><img :src="dateicon" alt=""><p class="date">{{ today }}</p></div> | |
9 | + <div class="buttons"> | |
10 | + <button><img :src="startbtn" alt=""></button> | |
11 | + <button><img :src="stopbtn" alt=""></button> | |
12 | + </div> | |
13 | + </div> | |
14 | + <div class="input-group"> | |
15 | + <select name="" id="" class="form-select"> | |
16 | + <option value="">년도</option> | |
17 | + </select> | |
18 | + <select name="" id="" class="form-select"> | |
19 | + <option value="">월</option> | |
20 | + </select> | |
21 | + <select name="" id="" class="form-select"> | |
22 | + <option value="">부서</option> | |
23 | + </select> | |
24 | + </div> | |
25 | + | |
26 | + </div> | |
27 | + </div> | |
28 | + <div class=" tbl-wrap tbl2"> | |
29 | + <table class="tbl data"> | |
30 | + <colgroup> | |
31 | + <col style="width: 150px;"> | |
32 | + <col style="width: "> | |
33 | + <col style="width: "> | |
34 | + <col style="width: "> | |
35 | + <col style="width: "> | |
36 | + <col style="width: "> | |
37 | + <!-- 더 많은 열 설정 --> | |
38 | + </colgroup> | |
39 | + <tbody> | |
40 | + <tr class="thead"> | |
41 | + <td rowspan="2" class="th">근태 현황</td> | |
42 | + <td>지각</td> | |
43 | + <td>조기퇴근</td> | |
44 | + <td>결근</td> | |
45 | + <td>출장</td> | |
46 | + <td>주말출근</td> | |
47 | + </tr> | |
48 | + <tr> | |
49 | + <td>{{ late }}</td> | |
50 | + <td>{{ earlyLeave }}</td> | |
51 | + <td>{{ absence }}</td> | |
52 | + <td>{{ businessTrip }}</td> | |
53 | + <td>{{ weekendWork }}</td> | |
54 | + </tr> | |
55 | + </tbody> | |
56 | + | |
57 | + </table> | |
58 | + <table class="tbl data"> | |
59 | + <colgroup> | |
60 | + <col style="width: 150px;"> | |
61 | + <col style="width: "> | |
62 | + <col style="width: "> | |
63 | + <col style="width: "> | |
64 | + <col style="width: "> | |
65 | + <!-- 더 많은 열 설정 --> | |
66 | + </colgroup> | |
67 | + <tbody> | |
68 | + <tr class="thead"> | |
69 | + <td rowspan="2" class="th">휴가 현황</td> | |
70 | + <td>연차</td> | |
71 | + <td>대체휴가</td> | |
72 | + <td>공가</td> | |
73 | + <td>병가</td> | |
74 | + </tr> | |
75 | + <tr> | |
76 | + <td></td> | |
77 | + <td></td> | |
78 | + <td></td> | |
79 | + <td></td> | |
80 | + </tr> | |
81 | + </tbody> | |
82 | + | |
83 | + </table> | |
84 | + </div> | |
85 | + <div class="tbl-wrap"> | |
86 | + <table id="myTable" class="tbl data"> | |
87 | + <colgroup> | |
88 | + <col style="width: 200px;"> | |
89 | + <col style=" width: "> | |
90 | + </colgroup> | |
91 | + <thead> | |
92 | + <tr> | |
93 | + <th>연차 </th> | |
94 | + <th>내용</th> | |
95 | + </tr> | |
96 | + </thead> | |
97 | + <!-- 동적으로 <td> 생성 --> | |
98 | + <tbody> | |
99 | + <tr v-for="(item, index) in listData" :key="index" > | |
100 | + <td>{{ item.type }}</td> | |
101 | + <td>{{ item.content }}</td> | |
102 | + </tr> | |
103 | + </tbody> | |
104 | + </table> | |
105 | + | |
106 | + </div> | |
107 | + <div class="pagination"> | |
108 | + <ul> | |
109 | + <!-- 왼쪽 화살표 (이전 페이지) --> | |
110 | + <li class="arrow" :class="{ disabled: currentPage === 1 }" @click="changePage(currentPage - 1)"> | |
111 | + < | |
112 | + </li> | |
113 | + | |
114 | + <!-- 페이지 번호 --> | |
115 | + <li v-for="page in totalPages" :key="page" :class="{ active: currentPage === page }" | |
116 | + @click="changePage(page)"> | |
117 | + {{ page }} | |
118 | + </li> | |
119 | + | |
120 | + <!-- 오른쪽 화살표 (다음 페이지) --> | |
121 | + <li class="arrow" :class="{ disabled: currentPage === totalPages }" @click="changePage(currentPage + 1)"> | |
122 | + > | |
123 | + </li> | |
124 | + </ul> | |
125 | + </div> | |
126 | + | |
127 | + </div> | |
128 | + </div> | |
129 | +</template> | |
130 | + | |
131 | +<script> | |
132 | +import { SearchOutlined } from '@ant-design/icons-vue'; | |
133 | +export default { | |
134 | + data() { | |
135 | + | |
136 | + const today = new Date().toISOString().split('T')[0]; | |
137 | + return { | |
138 | + currentPage: 1, | |
139 | + totalPages: 3, | |
140 | + late: '5', earlyLeave: '3', absence: '2', businessTrip: '1', weekendWork: '0' , | |
141 | + today: new Date().toLocaleDateString('ko-KR', { | |
142 | + year: 'numeric', | |
143 | + month: '2-digit', | |
144 | + day: '2-digit', | |
145 | + weekday: 'short', | |
146 | + }), | |
147 | + dateicon: "/client/resources/img/img.png", | |
148 | + startbtn: "/client/resources/img/start-sm.png", | |
149 | + stopbtn: "/client/resources/img/stop-sm.png", | |
150 | + startDate: today, | |
151 | + startTime: "09:00", // 기본 시작 시간 09:00 | |
152 | + endDate: today, | |
153 | + endTime: "18:00", // 기본 종료 시간 18:00 | |
154 | + category: "", | |
155 | + dayCount: 1, | |
156 | + reason: "", // 사유 | |
157 | + listData: [ | |
158 | + { | |
159 | + type: '연차', | |
160 | + content: '결재', | |
161 | + }, { | |
162 | + type: '반차', | |
163 | + content: '전결', | |
164 | + }], | |
165 | + }; | |
166 | + }, | |
167 | + components:{ | |
168 | + SearchOutlined | |
169 | + }, | |
170 | + computed: { | |
171 | + // Pinia Store의 상태를 가져옵니다. | |
172 | + loginUser() { | |
173 | + const authStore = useAuthStore(); | |
174 | + return authStore.getLoginUser; | |
175 | + }, | |
176 | + }, | |
177 | + methods: { | |
178 | + // 폼 검증 메서드 | |
179 | + validateForm() { | |
180 | + // 필수 입력 필드 체크 | |
181 | + if ( | |
182 | + this.category && | |
183 | + this.startDate && | |
184 | + this.startTime && | |
185 | + this.endDate && | |
186 | + this.endTime && | |
187 | + this.dayCount > 0 && | |
188 | + this.reason.trim() !== "" | |
189 | + ) { | |
190 | + this.isFormValid = true; | |
191 | + } else { | |
192 | + this.isFormValid = false; | |
193 | + } | |
194 | + }, | |
195 | + calculateDayCount() { | |
196 | + const start = new Date(`${this.startDate}T${this.startTime}:00`); | |
197 | + const end = new Date(`${this.endDate}T${this.endTime}:00`); | |
198 | + | |
199 | + let totalHours = (end - start) / (1000 * 60 * 60); // 밀리초를 시간 단위로 변환 | |
200 | + | |
201 | + if (this.startDate !== this.endDate) { | |
202 | + // 시작일과 종료일이 다른경우 | |
203 | + const startDateObj = new Date(this.startDate); | |
204 | + const endDateObj = new Date(this.endDate); | |
205 | + const daysDifference = (endDateObj - startDateObj) / (1000 * 60 * 60 * 24); // 두 날짜 사이의 차이를 일수로 계산 | |
206 | + if (this.startTime !== "09:00" || this.endTime !== "18:00") { | |
207 | + this.dayCount = daysDifference + 0.5; // 시간 조건이 기준에서 벗어날 경우 | |
208 | + } else { | |
209 | + this.dayCount = Math.ceil(daysDifference + 1); // 시간 조건이 기준에 맞을 경우 | |
210 | + } | |
211 | + } else { | |
212 | + // 시작일과 종료일이 같은 경우 | |
213 | + if (this.startTime !== "09:00" || this.endTime !== "18:00") { | |
214 | + this.dayCount = 0.5; // 시작 시간 또는 종료 시간이 기준과 다를 경우 0.5 | |
215 | + } else { | |
216 | + this.dayCount = 1; // 기준 시간(09:00~18:00)이 맞으면 1일로 간주 | |
217 | + } | |
218 | + } | |
219 | + | |
220 | + this.validateForm(); // dayCount 변경 후 폼 재검증 | |
221 | + }, | |
222 | + handleSubmit() { | |
223 | + this.validateForm(); // 제출 시 유효성 확인 | |
224 | + if (this.isFormValid) { | |
225 | + localStorage.setItem('HyugaFormData', JSON.stringify(this.$data)); | |
226 | + alert("승인 요청이 완료되었습니다."); | |
227 | + // 추가 처리 로직 (API 요청 등) | |
228 | + } else { | |
229 | + alert("모든 필드를 올바르게 작성해주세요."); | |
230 | + } | |
231 | + }, | |
232 | + | |
233 | + | |
234 | + }, | |
235 | + mounted() { | |
236 | + // Load the saved form data when the page is loaded | |
237 | + this.loadFormData(); | |
238 | + }, | |
239 | + watch: { | |
240 | + startDate: 'calculateDayCount', | |
241 | + startTime: 'calculateDayCount', | |
242 | + endDate: 'calculateDayCount', | |
243 | + endTime: 'calculateDayCount', | |
244 | + reason: "validateForm", | |
245 | + category: 'category', | |
246 | + }, | |
247 | +}; | |
248 | +</script> |
--- client/views/pages/User/MyPage.vue
+++ client/views/pages/User/MyPage.vue
... | ... | @@ -86,8 +86,9 @@ |
86 | 86 |
|
87 | 87 |
</form> |
88 | 88 |
<div class="buttons"> |
89 |
- <button class="btn btn-red w-100" type="submit">회원탈퇴</button> |
|
90 |
- <button class="btn secondary w-100" type="submit">수정</button> |
|
89 |
+ <button class="btn primary" type="submit">반려</button> |
|
90 |
+ <button class="btn btn-red " type="submit">반려</button> |
|
91 |
+ <button class="btn tertiary " type="submit">목록</button> |
|
91 | 92 |
</div> |
92 | 93 |
</div> |
93 | 94 |
</div> |
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?