907 lines
45 KiB
HTML
907 lines
45 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
|
<meta http-equiv="Pragma" content="no-cache">
|
|
<meta http-equiv="Expires" content="0">
|
|
<title>근태입력 조회</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
|
<script>
|
|
tailwind.config = {
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
primary: {
|
|
50: '#eff6ff',
|
|
100: '#dbeafe',
|
|
200: '#bfdbfe',
|
|
300: '#93c5fd',
|
|
400: '#60a5fa',
|
|
500: '#3b82f6',
|
|
600: '#2563eb',
|
|
700: '#1d4ed8',
|
|
800: '#1e40af',
|
|
900: '#1e3a8a',
|
|
},
|
|
success: {
|
|
50: '#f0fdf4',
|
|
100: '#dcfce7',
|
|
200: '#bbf7d0',
|
|
300: '#86efac',
|
|
400: '#4ade80',
|
|
500: '#22c55e',
|
|
600: '#16a34a',
|
|
700: '#15803d',
|
|
800: '#166534',
|
|
900: '#14532d',
|
|
},
|
|
warning: {
|
|
50: '#fffbeb',
|
|
100: '#fef3c7',
|
|
200: '#fde68a',
|
|
300: '#fcd34d',
|
|
400: '#fbbf24',
|
|
500: '#f59e0b',
|
|
600: '#d97706',
|
|
700: '#b45309',
|
|
800: '#92400e',
|
|
900: '#78350f',
|
|
},
|
|
danger: {
|
|
50: '#fef2f2',
|
|
100: '#fee2e2',
|
|
200: '#fecaca',
|
|
300: '#fca5a5',
|
|
400: '#f87171',
|
|
500: '#ef4444',
|
|
600: '#dc2626',
|
|
700: '#b91c1c',
|
|
800: '#991b1b',
|
|
900: '#7f1d1d',
|
|
}
|
|
},
|
|
animation: {
|
|
'fade-in': 'fadeIn 0.5s ease-in-out',
|
|
'slide-up': 'slideUp 0.3s ease-out',
|
|
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
|
},
|
|
keyframes: {
|
|
fadeIn: {
|
|
'0%': { opacity: '0' },
|
|
'100%': { opacity: '1' },
|
|
},
|
|
slideUp: {
|
|
'0%': { transform: 'translateY(10px)', opacity: '0' },
|
|
'100%': { transform: 'translateY(0)', opacity: '1' },
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
<style>
|
|
.glass-effect {
|
|
background: rgba(255, 255, 255, 0.25);
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(255, 255, 255, 0.18);
|
|
}
|
|
.gradient-bg {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
}
|
|
.card-hover {
|
|
transition: all 0.3s ease;
|
|
}
|
|
.card-hover:hover {
|
|
transform: translateY(-5px);
|
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
|
}
|
|
|
|
.loading {
|
|
display: inline-block;
|
|
width: 20px;
|
|
height: 20px;
|
|
border: 3px solid rgba(255, 255, 255, 0.3);
|
|
border-top: 3px solid #ffffff;
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
|
|
.table-container {
|
|
max-height: 600px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
/* 스크롤바 스타일링 */
|
|
.custom-scrollbar::-webkit-scrollbar {
|
|
width: var(--scrollbar-width, 16px);
|
|
}
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-track {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
|
background: rgba(255, 255, 255, 0.3);
|
|
border-radius: 8px;
|
|
border: 2px solid rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
|
background: rgba(255, 255, 255, 0.5);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="gradient-bg min-h-screen">
|
|
<div class="container mx-auto px-4 py-8">
|
|
|
|
<!-- 헤더 -->
|
|
<div class="text-center mb-8 animate-fade-in">
|
|
<h1 class="text-4xl font-bold text-white mb-2">근태관리</h1>
|
|
<p class="text-white/80 text-lg">출퇴근 시간 및 휴가 관리</p>
|
|
</div>
|
|
|
|
<!-- 개발중 경고 메시지 -->
|
|
<div class="bg-orange-500 rounded-lg p-4 mb-6 border-l-4 border-orange-700 animate-slide-up shadow-lg">
|
|
<div class="flex items-center">
|
|
<svg class="w-5 h-5 text-orange-900 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
|
|
</svg>
|
|
<div>
|
|
<p class="text-white font-bold text-base">🚧 개발중인 기능입니다</p>
|
|
<p class="text-orange-100 text-sm font-medium">일부 기능이 정상적으로 동작하지 않을 수 있습니다.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 검색 및 필터 섹션 -->
|
|
<div class="glass-effect rounded-lg p-6 mb-6 animate-slide-up">
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div>
|
|
<label for="startDate" class="block text-sm font-medium text-white/80 mb-2">시작일</label>
|
|
<input type="date" id="startDate" class="w-full px-3 py-2 bg-white/20 border border-white/30 rounded-md text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-white/50 focus:border-transparent">
|
|
</div>
|
|
<div>
|
|
<label for="endDate" class="block text-sm font-medium text-white/80 mb-2">종료일</label>
|
|
<input type="date" id="endDate" class="w-full px-3 py-2 bg-white/20 border border-white/30 rounded-md text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-white/50 focus:border-transparent">
|
|
</div>
|
|
<div class="flex items-end">
|
|
<button id="searchBtn" class="w-full glass-effect text-white px-4 py-2 rounded-md hover:bg-white/30 transition-colors duration-200 flex items-center justify-center">
|
|
<span id="searchBtnText">조회</span>
|
|
<div id="searchBtnLoading" class="loading ml-2 hidden"></div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 통계 카드 -->
|
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6 animate-slide-up">
|
|
<div class="glass-effect rounded-lg p-6 card-hover">
|
|
<div class="flex items-center">
|
|
<div class="p-3 bg-primary-500/20 rounded-lg">
|
|
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
|
</svg>
|
|
</div>
|
|
<div class="ml-4">
|
|
<p class="text-sm font-medium text-white/80">휴가사용</p>
|
|
<p id="totalDays" class="text-2xl font-bold text-white">0</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="glass-effect rounded-lg p-6 card-hover">
|
|
<div class="flex items-center">
|
|
<div class="p-3 bg-success-500/20 rounded-lg">
|
|
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
</svg>
|
|
</div>
|
|
<div class="ml-4">
|
|
<p class="text-sm font-medium text-white/80">대체사용</p>
|
|
<p id="normalDays" class="text-2xl font-bold text-white">0</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="glass-effect rounded-lg p-6 card-hover">
|
|
<div class="flex items-center">
|
|
<div class="p-3 bg-warning-500/20 rounded-lg">
|
|
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
</svg>
|
|
</div>
|
|
<div class="ml-4">
|
|
<p class="text-sm font-medium text-white/80">잔량(년차)</p>
|
|
<p id="lateDays" class="text-2xl font-bold text-white">0</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="glass-effect rounded-lg p-6 card-hover">
|
|
<div class="flex items-center">
|
|
<div class="p-3 bg-danger-500/20 rounded-lg">
|
|
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
</svg>
|
|
</div>
|
|
<div class="ml-4">
|
|
<p class="text-sm font-medium text-white/80">잔량(대체)</p>
|
|
<p id="absentDays" class="text-2xl font-bold text-white">0</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 데이터 테이블 -->
|
|
<div class="glass-effect rounded-lg animate-slide-up custom-scrollbar">
|
|
<div class="px-6 py-4 border-b border-white/20 flex justify-between items-center">
|
|
<h3 class="text-lg font-medium text-white">근태 상세 내역</h3>
|
|
<button id="addBtn" class="glass-effect text-white px-4 py-2 rounded-md hover:bg-white/30 transition-colors duration-200 flex items-center">
|
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
|
</svg>
|
|
근태 추가
|
|
</button>
|
|
</div>
|
|
<div class="table-container">
|
|
<table class="min-w-full divide-y divide-white/20">
|
|
<thead class="bg-white/10 sticky top-0">
|
|
<tr>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">구분</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">시작일</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">종료일</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">사번</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">성명</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">사용(일)</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">사용(H)</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">발생(일)</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">발생(H)</th>
|
|
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">내용</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">#</th>
|
|
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">잔량(일)</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">잔량(H)</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">전일(일)</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">전일(H)</th>
|
|
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">소스</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">등록자</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">등록일</th>
|
|
|
|
|
|
</tr>
|
|
</thead>
|
|
<tbody id="dataTableBody" class="divide-y divide-white/10">
|
|
<tr id="loadingRow" class="hidden">
|
|
<td colspan="18" class="px-6 py-4 text-center">
|
|
<div class="flex items-center justify-center">
|
|
<div class="loading mr-2"></div>
|
|
<span class="text-white/80">데이터를 불러오는 중...</span>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr id="noDataRow" class="hidden">
|
|
<td colspan="18" class="px-6 py-4 text-center text-white/70">
|
|
조회된 데이터가 없습니다.
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 근태 추가/편집 모달 -->
|
|
<div id="kuntaeModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full hidden z-50">
|
|
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
|
|
<div class="mt-3">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h3 id="modalTitle" class="text-lg font-medium text-gray-900">근태 추가</h3>
|
|
<button id="closeModal" class="text-gray-400 hover:text-gray-600">
|
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<form id="kuntaeForm">
|
|
<input type="hidden" id="editId" name="id">
|
|
|
|
<div class="mb-4">
|
|
<label for="modalDate" class="block text-sm font-medium text-gray-700 mb-2">날짜</label>
|
|
<input type="date" id="modalDate" name="pdate" required
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4 mb-4">
|
|
<div>
|
|
<label for="modalInTime" class="block text-sm font-medium text-gray-700 mb-2">출근시간</label>
|
|
<input type="time" id="modalInTime" name="intime"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
|
</div>
|
|
<div>
|
|
<label for="modalOutTime" class="block text-sm font-medium text-gray-700 mb-2">퇴근시간</label>
|
|
<input type="time" id="modalOutTime" name="outtime"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<label for="modalMemo" class="block text-sm font-medium text-gray-700 mb-2">비고</label>
|
|
<textarea id="modalMemo" name="memo" rows="3"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
|
placeholder="비고사항을 입력하세요"></textarea>
|
|
</div>
|
|
|
|
<div class="flex justify-end space-x-3">
|
|
<button type="button" id="cancelBtn"
|
|
class="px-4 py-2 text-gray-700 bg-gray-200 rounded-md hover:bg-gray-300 transition-colors duration-200">
|
|
취소
|
|
</button>
|
|
<button type="submit" id="saveBtn"
|
|
class="px-4 py-2 bg-primary text-white rounded-md hover:bg-blue-600 transition-colors duration-200">
|
|
저장
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 삭제 확인 모달 -->
|
|
<div id="deleteModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full hidden z-50">
|
|
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
|
|
<div class="mt-3">
|
|
<div class="flex items-center mb-4">
|
|
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100">
|
|
<svg class="h-6 w-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<div class="text-center">
|
|
<h3 class="text-lg font-medium text-gray-900 mb-2">근태 삭제</h3>
|
|
<p class="text-sm text-gray-500 mb-4">선택한 근태 데이터를 삭제하시겠습니까?</p>
|
|
<p id="deleteConfirmText" class="text-sm text-gray-700 mb-6"></p>
|
|
<div class="flex justify-center space-x-3">
|
|
<button id="cancelDeleteBtn"
|
|
class="px-4 py-2 text-gray-700 bg-gray-200 rounded-md hover:bg-gray-300 transition-colors duration-200">
|
|
취소
|
|
</button>
|
|
<button id="confirmDeleteBtn"
|
|
class="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors duration-200">
|
|
삭제
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// 공통 네비게이션 컴포넌트
|
|
class CommonNavigation {
|
|
constructor(currentPage = '') {
|
|
this.currentPage = currentPage;
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
this.createNavigation();
|
|
this.addEventListeners();
|
|
}
|
|
|
|
createNavigation() {
|
|
const nav = document.createElement('nav');
|
|
nav.className = 'glass-effect border-b border-white/10';
|
|
nav.style.cssText = `
|
|
background: rgba(255, 255, 255, 0.25);
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(255, 255, 255, 0.18);
|
|
`;
|
|
nav.innerHTML = this.getNavigationHTML();
|
|
|
|
// body의 첫 번째 자식으로 추가
|
|
document.body.insertBefore(nav, document.body.firstChild);
|
|
}
|
|
|
|
getNavigationHTML() {
|
|
return `
|
|
<div class="container mx-auto px-4" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
|
|
<div class="flex items-center justify-between h-16">
|
|
<!-- 로고/타이틀 -->
|
|
<div class="flex items-center">
|
|
<h2 class="text-xl font-bold text-white">GroupWare</h2>
|
|
</div>
|
|
|
|
<!-- 메뉴 -->
|
|
<div class="hidden md:flex items-center space-x-8">
|
|
${this.getMenuItemHTML('dashboard', '/Dashboard/', '대시보드', 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z')}
|
|
${this.getMenuItemHTML('common', '/Common', '공용코드', 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z')}
|
|
${this.getMenuItemHTML('jobreport', '/Jobreport/', '업무일지', 'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2')}
|
|
${this.getMenuItemHTML('kuntae', '/Kuntae/', '근태관리', 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z')}
|
|
</div>
|
|
|
|
<!-- 모바일 메뉴 버튼 -->
|
|
<div class="md:hidden">
|
|
<button id="mobile-menu-button" class="text-white/80 hover:text-white transition-colors p-2">
|
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 모바일 메뉴 -->
|
|
<div id="mobile-menu" class="md:hidden hidden border-t border-white/10 pt-4 pb-4">
|
|
${this.getMobileMenuItemHTML('dashboard', '/Dashboard/', '대시보드', 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z')}
|
|
${this.getMobileMenuItemHTML('common', '/Common', '공용코드', 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z')}
|
|
${this.getMobileMenuItemHTML('jobreport', '/Jobreport/', '업무일지', 'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2')}
|
|
${this.getMobileMenuItemHTML('kuntae', '/Kuntae/', '근태관리', 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
getMenuItemHTML(pageKey, href, text, svgPath) {
|
|
const isActive = this.currentPage === pageKey;
|
|
const activeClass = isActive ? 'text-white bg-white/20' : 'text-white/80 hover:text-white hover:bg-white/10';
|
|
|
|
return `
|
|
<a href="${href}" class="${activeClass} transition-colors px-3 py-2 rounded-lg">
|
|
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${svgPath}"></path>
|
|
</svg>
|
|
${text}
|
|
</a>
|
|
`;
|
|
}
|
|
|
|
getMobileMenuItemHTML(pageKey, href, text, svgPath) {
|
|
const isActive = this.currentPage === pageKey;
|
|
const activeClass = isActive ? 'text-white bg-white/20' : 'text-white/80 hover:text-white hover:bg-white/10';
|
|
|
|
return `
|
|
<a href="${href}" class="block ${activeClass} transition-colors px-3 py-2 rounded-lg mb-2">
|
|
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${svgPath}"></path>
|
|
</svg>
|
|
${text}
|
|
</a>
|
|
`;
|
|
}
|
|
|
|
addEventListeners() {
|
|
// 모바일 메뉴 토글
|
|
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
|
const mobileMenu = document.getElementById('mobile-menu');
|
|
|
|
if (mobileMenuButton && mobileMenu) {
|
|
mobileMenuButton.addEventListener('click', function() {
|
|
mobileMenu.classList.toggle('hidden');
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// 전역 함수로 내비게이션 초기화
|
|
function initNavigation(currentPage = '') {
|
|
// DOM이 로드된 후에 실행
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
new CommonNavigation(currentPage);
|
|
});
|
|
} else {
|
|
new CommonNavigation(currentPage);
|
|
}
|
|
}
|
|
|
|
// 전역 변수
|
|
let currentData = [];
|
|
let currentEditId = null;
|
|
|
|
// 페이지 로드 시 초기화
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// 네비게이션 초기화
|
|
initNavigation('kuntae');
|
|
initializeDates();
|
|
loadData();
|
|
setupEventListeners();
|
|
setupModalEvents();
|
|
});
|
|
|
|
// 날짜 초기화 (현재 월)
|
|
function initializeDates() {
|
|
const now = new Date();
|
|
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
|
|
|
document.getElementById('startDate').value = startOfMonth.toISOString().split('T')[0];
|
|
document.getElementById('endDate').value = endOfMonth.toISOString().split('T')[0];
|
|
}
|
|
|
|
// 이벤트 리스너 설정
|
|
function setupEventListeners() {
|
|
document.getElementById('searchBtn').addEventListener('click', loadData);
|
|
document.getElementById('addBtn').addEventListener('click', showAddModal);
|
|
|
|
// Enter 키로 검색
|
|
document.getElementById('startDate').addEventListener('keypress', function(e) {
|
|
if (e.key === 'Enter') loadData();
|
|
});
|
|
document.getElementById('endDate').addEventListener('keypress', function(e) {
|
|
if (e.key === 'Enter') loadData();
|
|
});
|
|
}
|
|
|
|
// 모달 이벤트 설정
|
|
function setupModalEvents() {
|
|
// 근태 모달 이벤트
|
|
document.getElementById('closeModal').addEventListener('click', hideKuntaeModal);
|
|
document.getElementById('cancelBtn').addEventListener('click', hideKuntaeModal);
|
|
document.getElementById('kuntaeForm').addEventListener('submit', saveKuntae);
|
|
|
|
// 삭제 모달 이벤트
|
|
document.getElementById('cancelDeleteBtn').addEventListener('click', hideDeleteModal);
|
|
document.getElementById('confirmDeleteBtn').addEventListener('click', confirmDelete);
|
|
|
|
// 모달 외부 클릭 시 닫기
|
|
document.getElementById('kuntaeModal').addEventListener('click', function(e) {
|
|
if (e.target === this) hideKuntaeModal();
|
|
});
|
|
document.getElementById('deleteModal').addEventListener('click', function(e) {
|
|
if (e.target === this) hideDeleteModal();
|
|
});
|
|
}
|
|
|
|
// 데이터 로드
|
|
async function loadData() {
|
|
const startDate = document.getElementById('startDate').value;
|
|
const endDate = document.getElementById('endDate').value;
|
|
|
|
if (!startDate || !endDate) {
|
|
alert('시작일과 종료일을 모두 입력해주세요.');
|
|
return;
|
|
}
|
|
|
|
if (new Date(startDate) > new Date(endDate)) {
|
|
alert('시작일은 종료일보다 늦을 수 없습니다.');
|
|
return;
|
|
}
|
|
|
|
showLoading(true);
|
|
|
|
try {
|
|
const response = await axios.get(`/Kuntae/GetList?sd=${startDate}&ed=${endDate}`);
|
|
|
|
if (response.data) {
|
|
currentData = response.data;
|
|
renderTable();
|
|
updateStatistics();
|
|
} else {
|
|
currentData = [];
|
|
renderTable();
|
|
}
|
|
} catch (error) {
|
|
console.error('데이터 로드 중 오류 발생:', error);
|
|
alert('데이터를 불러오는 중 오류가 발생했습니다.');
|
|
currentData = [];
|
|
renderTable();
|
|
} finally {
|
|
showLoading(false);
|
|
}
|
|
}
|
|
|
|
// 로딩 상태 표시
|
|
function showLoading(show) {
|
|
const searchBtn = document.getElementById('searchBtn');
|
|
const searchBtnText = document.getElementById('searchBtnText');
|
|
const searchBtnLoading = document.getElementById('searchBtnLoading');
|
|
const loadingRow = document.getElementById('loadingRow');
|
|
|
|
if (show) {
|
|
searchBtn.disabled = true;
|
|
searchBtnText.textContent = '조회 중...';
|
|
searchBtnLoading.classList.remove('hidden');
|
|
loadingRow.classList.remove('hidden');
|
|
} else {
|
|
searchBtn.disabled = false;
|
|
searchBtnText.textContent = '조회';
|
|
searchBtnLoading.classList.add('hidden');
|
|
loadingRow.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
// 테이블 렌더링
|
|
function renderTable() {
|
|
const tbody = document.getElementById('dataTableBody');
|
|
const noDataRow = document.getElementById('noDataRow');
|
|
|
|
// 기존 데이터 행 제거 (로딩, 노데이터 행 제외)
|
|
const existingRows = tbody.querySelectorAll('tr:not(#loadingRow):not(#noDataRow)');
|
|
existingRows.forEach(row => row.remove());
|
|
|
|
if (currentData.length === 0) {
|
|
noDataRow.classList.remove('hidden');
|
|
return;
|
|
}
|
|
|
|
noDataRow.classList.add('hidden');
|
|
|
|
currentData.forEach(item => {
|
|
const row = document.createElement('tr');
|
|
row.className = 'hover:bg-white/10 cursor-pointer transition-colors';
|
|
row.setAttribute('data-id', item.idx);
|
|
|
|
const startDate = item.sdate ? new Date(item.sdate) : null;
|
|
const endDate = item.edate ? new Date(item.edate) : null;
|
|
|
|
row.innerHTML = `
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.cate || '-'}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${startDate ? formatDate(startDate) : '-'}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${endDate ? formatDate(endDate) : '-'}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.uid || '-'}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.uname || '-'}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.term || '-'}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.termdr || '-'}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.drtime || '-'}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.crtime || '-'}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white/80 max-w-xs truncate" title="${item.contents || ''}">${item.contents || '-'}</td>
|
|
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.tag || '-'}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white"> </td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white"> </td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white"> </td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white"> </td>
|
|
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.extcate || '-'}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.wuid || '-'}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.wdate || '-'}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-white/80">
|
|
<div class="flex space-x-2">
|
|
<button class="text-blue-400 hover:text-blue-300 edit-btn transition-colors" data-id="${item.idx}">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
|
</svg>
|
|
</button>
|
|
<button class="text-red-400 hover:text-red-300 delete-btn transition-colors" data-id="${item.idx}">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
|
|
`;
|
|
|
|
tbody.appendChild(row);
|
|
});
|
|
|
|
// 행 클릭 이벤트 추가
|
|
tbody.querySelectorAll('tr[data-id]').forEach(row => {
|
|
row.addEventListener('click', function(e) {
|
|
if (!e.target.closest('.edit-btn') && !e.target.closest('.delete-btn')) {
|
|
const id = this.getAttribute('data-id');
|
|
showEditModal(id);
|
|
}
|
|
});
|
|
});
|
|
|
|
// 편집 버튼 이벤트
|
|
tbody.querySelectorAll('.edit-btn').forEach(btn => {
|
|
btn.addEventListener('click', function(e) {
|
|
e.stopPropagation();
|
|
const id = this.getAttribute('data-id');
|
|
showEditModal(id);
|
|
});
|
|
});
|
|
|
|
// 삭제 버튼 이벤트
|
|
tbody.querySelectorAll('.delete-btn').forEach(btn => {
|
|
btn.addEventListener('click', function(e) {
|
|
e.stopPropagation();
|
|
const id = this.getAttribute('data-id');
|
|
showDeleteModal(id);
|
|
});
|
|
});
|
|
}
|
|
|
|
// 날짜 포맷팅
|
|
function formatDate(date) {
|
|
return date.toLocaleDateString('ko-KR', {
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit'
|
|
});
|
|
}
|
|
|
|
// 근무시간 계산
|
|
function calculateWorkHours(item) {
|
|
if (!item.intime || !item.outtime) return '-';
|
|
|
|
try {
|
|
const inTime = new Date(`2000-01-01 ${item.intime}`);
|
|
const outTime = new Date(`2000-01-01 ${item.outtime}`);
|
|
|
|
if (outTime < inTime) {
|
|
outTime.setDate(outTime.getDate() + 1);
|
|
}
|
|
|
|
const diffMs = outTime - inTime;
|
|
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
|
|
|
return `${diffHours}시간 ${diffMinutes}분`;
|
|
} catch (error) {
|
|
return '-';
|
|
}
|
|
}
|
|
|
|
// 상태 판단
|
|
function getStatus(item) {
|
|
if (!item.intime) return '결근';
|
|
if (!item.outtime) return '출근';
|
|
|
|
// 지각 판단 (예: 9시 이후 출근)
|
|
const inTime = new Date(`2000-01-01 ${item.intime}`);
|
|
const lateThreshold = new Date(`2000-01-01 09:00:00`);
|
|
|
|
if (inTime > lateThreshold) return '지각';
|
|
return '정상';
|
|
}
|
|
|
|
// 상태별 CSS 클래스
|
|
function getStatusClass(status) {
|
|
switch (status) {
|
|
case '정상': return 'bg-green-100 text-green-800';
|
|
case '지각': return 'bg-yellow-100 text-yellow-800';
|
|
case '결근': return 'bg-red-100 text-red-800';
|
|
case '출근': return 'bg-blue-100 text-blue-800';
|
|
default: return 'bg-gray-100 text-gray-800';
|
|
}
|
|
}
|
|
|
|
// 통계 업데이트
|
|
function updateStatistics() {
|
|
const totalDays = currentData.length;
|
|
const normalDays = currentData.filter(item => getStatus(item) === '정상').length;
|
|
const lateDays = currentData.filter(item => getStatus(item) === '지각').length;
|
|
const absentDays = currentData.filter(item => getStatus(item) === '결근').length;
|
|
|
|
document.getElementById('totalDays').textContent = totalDays;
|
|
document.getElementById('normalDays').textContent = normalDays;
|
|
document.getElementById('lateDays').textContent = lateDays;
|
|
document.getElementById('absentDays').textContent = absentDays;
|
|
}
|
|
|
|
// 근태 추가 모달 표시
|
|
function showAddModal() {
|
|
currentEditId = null;
|
|
document.getElementById('modalTitle').textContent = '근태 추가';
|
|
document.getElementById('editId').value = '';
|
|
document.getElementById('modalDate').value = new Date().toISOString().split('T')[0];
|
|
document.getElementById('modalInTime').value = '';
|
|
document.getElementById('modalOutTime').value = '';
|
|
document.getElementById('modalMemo').value = '';
|
|
document.getElementById('kuntaeModal').classList.remove('hidden');
|
|
}
|
|
|
|
// 근태 편집 모달 표시
|
|
function showEditModal(id) {
|
|
const item = currentData.find(data => (data.id || data.wdate) == id);
|
|
if (!item) {
|
|
alert('데이터를 찾을 수 없습니다.');
|
|
return;
|
|
}
|
|
|
|
currentEditId = id;
|
|
document.getElementById('modalTitle').textContent = '근태 편집';
|
|
document.getElementById('editId').value = id;
|
|
document.getElementById('modalDate').value = item.pdate ? new Date(item.pdate).toISOString().split('T')[0] : '';
|
|
document.getElementById('modalInTime').value = item.intime || '';
|
|
document.getElementById('modalOutTime').value = item.outtime || '';
|
|
document.getElementById('modalMemo').value = item.memo || '';
|
|
document.getElementById('kuntaeModal').classList.remove('hidden');
|
|
}
|
|
|
|
// 근태 모달 숨기기
|
|
function hideKuntaeModal() {
|
|
document.getElementById('kuntaeModal').classList.add('hidden');
|
|
currentEditId = null;
|
|
}
|
|
|
|
// 근태 저장
|
|
async function saveKuntae(e) {
|
|
e.preventDefault();
|
|
|
|
const formData = new FormData(e.target);
|
|
const data = {
|
|
id: formData.get('id'),
|
|
pdate: formData.get('pdate'),
|
|
intime: formData.get('intime'),
|
|
outtime: formData.get('outtime'),
|
|
memo: formData.get('memo')
|
|
};
|
|
|
|
if (!data.pdate) {
|
|
alert('날짜를 입력해주세요.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const url = currentEditId ? '/Kuntae/Update' : '/Kuntae/Insert';
|
|
const method = currentEditId ? 'PUT' : 'POST';
|
|
|
|
const response = await axios({
|
|
method: method,
|
|
url: url,
|
|
data: data,
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
if (response.data && response.data.success) {
|
|
alert(currentEditId ? '근태가 수정되었습니다.' : '근태가 추가되었습니다.');
|
|
hideKuntaeModal();
|
|
loadData(); // 목록 새로고침
|
|
} else {
|
|
alert(response.data?.message || '저장 중 오류가 발생했습니다.');
|
|
}
|
|
} catch (error) {
|
|
console.error('저장 중 오류 발생:', error);
|
|
alert('저장 중 오류가 발생했습니다.');
|
|
}
|
|
}
|
|
|
|
// 삭제 모달 표시
|
|
function showDeleteModal(id) {
|
|
const item = currentData.find(data => (data.id || data.wdate) == id);
|
|
if (!item) {
|
|
alert('데이터를 찾을 수 없습니다.');
|
|
return;
|
|
}
|
|
|
|
const date = new Date(item.pdate);
|
|
document.getElementById('deleteConfirmText').textContent =
|
|
`${formatDate(date)} 근태 데이터를 삭제하시겠습니까?`;
|
|
|
|
document.getElementById('confirmDeleteBtn').setAttribute('data-id', id);
|
|
document.getElementById('deleteModal').classList.remove('hidden');
|
|
}
|
|
|
|
// 삭제 모달 숨기기
|
|
function hideDeleteModal() {
|
|
document.getElementById('deleteModal').classList.add('hidden');
|
|
}
|
|
|
|
// 삭제 확인
|
|
async function confirmDelete() {
|
|
const id = document.getElementById('confirmDeleteBtn').getAttribute('data-id');
|
|
|
|
try {
|
|
const response = await axios.delete(`/Kuntae/Delete/${id}`);
|
|
|
|
if (response.data && response.data.success) {
|
|
alert('근태가 삭제되었습니다.');
|
|
hideDeleteModal();
|
|
loadData(); // 목록 새로고침
|
|
} else {
|
|
alert(response.data?.message || '삭제 중 오류가 발생했습니다.');
|
|
}
|
|
} catch (error) {
|
|
console.error('삭제 중 오류 발생:', error);
|
|
alert('삭제 중 오류가 발생했습니다.');
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|