feat: 캘린더 기반 작업 현황 확인 시스템 구현
- 월별 캘린더 UI로 작업 현황을 한눈에 확인 가능 - 미입력(빨강), 부분입력(주황), 확인필요(보라), 이상없음(초록) 상태 표시 - 범례 아이콘(●)을 사용한 직관적인 상태 표시 - 날짜 클릭 시 해당일 작업자별 상세 현황 모달 - 작업자 클릭 시 개별 작업 입력/수정 모달 - 휴가 처리 기능 (연차, 반차, 반반차, 조퇴) - 월별 집계 데이터 최적화로 API 호출 최소화 백엔드: - monthly_worker_status, monthly_summary 테이블 추가 - 자동 집계 stored procedure 및 trigger 구현 - 확인필요(12시간 초과) 상태 감지 로직 - 출석 관리 시스템 확장 프론트엔드: - 캘린더 그리드 UI 구현 - 상태별 색상 및 아이콘 표시 - 모달 기반 상세 정보 표시 - 반응형 디자인 적용
This commit is contained in:
166
web-ui/css/common.css
Normal file
166
web-ui/css/common.css
Normal file
@@ -0,0 +1,166 @@
|
||||
/* Common CSS - 공통 스타일 */
|
||||
|
||||
/* Reset and Base Styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 { font-size: 2rem; }
|
||||
h2 { font-size: 1.5rem; }
|
||||
h3 { font-size: 1.25rem; }
|
||||
h4 { font-size: 1.125rem; }
|
||||
h5 { font-size: 1rem; }
|
||||
h6 { font-size: 0.875rem; }
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #2563eb;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #6b7280;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #4b5563;
|
||||
}
|
||||
|
||||
/* Utilities */
|
||||
.text-center { text-align: center; }
|
||||
.text-left { text-align: left; }
|
||||
.text-right { text-align: right; }
|
||||
|
||||
.mb-1 { margin-bottom: 0.25rem; }
|
||||
.mb-2 { margin-bottom: 0.5rem; }
|
||||
.mb-3 { margin-bottom: 0.75rem; }
|
||||
.mb-4 { margin-bottom: 1rem; }
|
||||
.mb-5 { margin-bottom: 1.25rem; }
|
||||
.mb-6 { margin-bottom: 1.5rem; }
|
||||
|
||||
.mt-1 { margin-top: 0.25rem; }
|
||||
.mt-2 { margin-top: 0.5rem; }
|
||||
.mt-3 { margin-top: 0.75rem; }
|
||||
.mt-4 { margin-top: 1rem; }
|
||||
.mt-5 { margin-top: 1.25rem; }
|
||||
.mt-6 { margin-top: 1.5rem; }
|
||||
|
||||
.p-1 { padding: 0.25rem; }
|
||||
.p-2 { padding: 0.5rem; }
|
||||
.p-3 { padding: 0.75rem; }
|
||||
.p-4 { padding: 1rem; }
|
||||
.p-5 { padding: 1.25rem; }
|
||||
.p-6 { padding: 1.5rem; }
|
||||
|
||||
/* Container */
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.25rem;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
/* Loading */
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #3b82f6;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
h1 { font-size: 1.5rem; }
|
||||
h2 { font-size: 1.25rem; }
|
||||
h3 { font-size: 1.125rem; }
|
||||
}
|
||||
@@ -817,4 +817,346 @@
|
||||
.guide-item strong {
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 개별 작업 보고서 전용 스타일 ========== */
|
||||
|
||||
/* 작업자 정보 카드 */
|
||||
.worker-info-card {
|
||||
background: linear-gradient(135deg, var(--primary-50) 0%, var(--secondary-50) 100%);
|
||||
border: 2px solid var(--primary-200);
|
||||
border-radius: var(--radius-2xl);
|
||||
padding: var(--space-8);
|
||||
margin-bottom: var(--space-8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-6);
|
||||
box-shadow: var(--shadow-lg);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.worker-info-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, var(--primary-500), var(--secondary-500));
|
||||
}
|
||||
|
||||
.worker-avatar-large {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: var(--radius-full);
|
||||
background: linear-gradient(135deg, var(--primary-500), var(--secondary-500));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-inverse);
|
||||
font-weight: var(--font-bold);
|
||||
font-size: var(--text-3xl);
|
||||
box-shadow: var(--shadow-lg);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.worker-info-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.worker-info-details h2 {
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: var(--font-bold);
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 var(--space-2) 0;
|
||||
}
|
||||
|
||||
.worker-info-details p {
|
||||
font-size: var(--text-base);
|
||||
color: var(--text-secondary);
|
||||
margin: 0 0 var(--space-1) 0;
|
||||
}
|
||||
|
||||
.worker-status-summary {
|
||||
display: flex;
|
||||
gap: var(--space-6);
|
||||
}
|
||||
|
||||
.status-item {
|
||||
text-align: center;
|
||||
padding: var(--space-4);
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--radius-xl);
|
||||
border: 1px solid var(--border-light);
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
display: block;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: var(--space-1);
|
||||
}
|
||||
|
||||
.status-value {
|
||||
display: block;
|
||||
font-size: var(--text-xl);
|
||||
font-weight: var(--font-bold);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.status-value.warning {
|
||||
color: var(--error-600);
|
||||
}
|
||||
|
||||
/* 섹션 헤더 */
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--space-6);
|
||||
padding-bottom: var(--space-4);
|
||||
border-bottom: 2px solid var(--border-light);
|
||||
}
|
||||
|
||||
.section-header h3 {
|
||||
font-size: var(--text-xl);
|
||||
font-weight: var(--font-bold);
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 기존 작업 목록 */
|
||||
.existing-work-section {
|
||||
margin-bottom: var(--space-8);
|
||||
}
|
||||
|
||||
.existing-work-item {
|
||||
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-tertiary) 100%);
|
||||
border: 2px solid var(--border-light);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: var(--space-6);
|
||||
margin-bottom: var(--space-4);
|
||||
transition: var(--transition-normal);
|
||||
box-shadow: var(--shadow-md);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.existing-work-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: var(--success-500);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.existing-work-item:hover {
|
||||
border-color: var(--primary-300);
|
||||
box-shadow: var(--shadow-lg);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.existing-work-item:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.work-item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.work-item-info h4 {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 var(--space-1) 0;
|
||||
}
|
||||
|
||||
.work-item-info p {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.work-item-status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: var(--space-1) var(--space-3);
|
||||
border-radius: var(--radius-full);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: var(--font-medium);
|
||||
}
|
||||
|
||||
.status-badge.normal {
|
||||
background: var(--success-100);
|
||||
color: var(--success-700);
|
||||
border: 1px solid var(--success-300);
|
||||
}
|
||||
|
||||
.status-badge.error {
|
||||
background: var(--error-100);
|
||||
color: var(--error-700);
|
||||
border: 1px solid var(--error-300);
|
||||
}
|
||||
|
||||
.work-hours {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: var(--font-bold);
|
||||
color: var(--primary-600);
|
||||
}
|
||||
|
||||
.work-item-error {
|
||||
background: var(--error-50);
|
||||
border: 1px solid var(--error-200);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-3);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.error-label {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--error-600);
|
||||
}
|
||||
|
||||
.error-type {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--error-700);
|
||||
margin-left: var(--space-2);
|
||||
}
|
||||
|
||||
.work-item-actions {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 새 작업 추가 섹션 */
|
||||
.new-work-section {
|
||||
margin-bottom: var(--space-8);
|
||||
}
|
||||
|
||||
/* 휴가 처리 섹션 */
|
||||
.vacation-section {
|
||||
margin-bottom: var(--space-8);
|
||||
}
|
||||
|
||||
.vacation-buttons {
|
||||
display: flex;
|
||||
gap: var(--space-4);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.vacation-process-btn {
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
padding: var(--space-4) var(--space-6);
|
||||
font-size: var(--text-base);
|
||||
font-weight: var(--font-medium);
|
||||
background: linear-gradient(135deg, var(--warning-500), var(--warning-600));
|
||||
border: none;
|
||||
color: var(--text-inverse);
|
||||
border-radius: var(--radius-xl);
|
||||
transition: var(--transition-normal);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.vacation-process-btn:hover {
|
||||
background: linear-gradient(135deg, var(--warning-600), var(--warning-700));
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
/* 빈 상태 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: var(--space-12);
|
||||
background: var(--bg-secondary);
|
||||
border-radius: var(--radius-xl);
|
||||
border: 2px dashed var(--border-light);
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: var(--text-6xl);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
font-size: var(--text-xl);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 var(--space-2) 0;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
font-size: var(--text-base);
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 개별 보고서 반응형 디자인 */
|
||||
@media (max-width: 768px) {
|
||||
.worker-info-card {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.worker-status-summary {
|
||||
justify-content: center;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.work-item-header {
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.work-item-status {
|
||||
align-items: flex-start;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.vacation-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.vacation-process-btn {
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.worker-status-summary {
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.status-item {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.work-item-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
1565
web-ui/css/work-report-calendar.css
Normal file
1565
web-ui/css/work-report-calendar.css
Normal file
File diff suppressed because it is too large
Load Diff
@@ -82,23 +82,26 @@ function getAuthHeaders() {
|
||||
}
|
||||
|
||||
// 🔧 개선된 API 호출 함수 (에러 처리 강화)
|
||||
async function apiCall(url, options = {}) {
|
||||
const defaultOptions = {
|
||||
headers: getAuthHeaders()
|
||||
};
|
||||
|
||||
const finalOptions = {
|
||||
...defaultOptions,
|
||||
...options,
|
||||
async function apiCall(url, method = 'GET', data = null) {
|
||||
// 상대 경로를 절대 경로로 변환
|
||||
const fullUrl = url.startsWith('http') ? url : `${API}${url}`;
|
||||
|
||||
const options = {
|
||||
method: method,
|
||||
headers: {
|
||||
...defaultOptions.headers,
|
||||
...options.headers
|
||||
'Content-Type': 'application/json',
|
||||
...getAuthHeaders()
|
||||
}
|
||||
};
|
||||
|
||||
// POST/PUT 요청시 데이터 추가
|
||||
if (data && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
|
||||
options.body = JSON.stringify(data);
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`📡 API 호출: ${url}`);
|
||||
const response = await fetch(url, finalOptions);
|
||||
console.log(`📡 API 호출: ${fullUrl} (${method})`);
|
||||
const response = await fetch(fullUrl, options);
|
||||
|
||||
// 인증 만료 처리
|
||||
if (response.status === 401) {
|
||||
@@ -122,11 +125,11 @@ async function apiCall(url, options = {}) {
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log(`✅ API 성공: ${url}`);
|
||||
console.log(`✅ API 성공: ${fullUrl}`);
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ API 오류 (${url}):`, error);
|
||||
console.error(`❌ API 오류 (${fullUrl}):`, error);
|
||||
|
||||
// 네트워크 오류 vs 서버 오류 구분
|
||||
if (error.name === 'TypeError' && error.message.includes('fetch')) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// js/load-navbar.js
|
||||
import { getUser, clearAuthData } from './auth.js';
|
||||
// 브라우저 호환 버전 - ES6 모듈 제거
|
||||
|
||||
// 역할 이름을 한글로 변환하는 맵
|
||||
const ROLE_NAMES = {
|
||||
|
||||
@@ -19,6 +19,14 @@ let workersData = [];
|
||||
let workData = [];
|
||||
let selectedDate = new Date().toISOString().split('T')[0];
|
||||
|
||||
// 모달 관련 변수
|
||||
let currentModalWorker = null;
|
||||
let modalWorkTypes = [];
|
||||
let modalWorkStatusTypes = [];
|
||||
let modalErrorTypes = [];
|
||||
let modalProjects = [];
|
||||
let modalExistingWork = [];
|
||||
|
||||
// DOM 요소
|
||||
const elements = {
|
||||
currentTime: document.getElementById('currentTime'),
|
||||
@@ -207,7 +215,7 @@ async function loadDashboardData() {
|
||||
async function loadWorkers() {
|
||||
try {
|
||||
console.log('👥 작업자 데이터 로딩...');
|
||||
const response = await window.apiCall(`${window.API}/workers`);
|
||||
const response = await window.apiCall('/workers');
|
||||
workersData = Array.isArray(response) ? response : (response.data || []);
|
||||
console.log(`✅ 작업자 ${workersData.length}명 로드 완료`);
|
||||
return workersData;
|
||||
@@ -221,7 +229,7 @@ async function loadWorkers() {
|
||||
async function loadWorkData(date) {
|
||||
try {
|
||||
console.log(`📋 ${date} 작업 데이터 로딩...`);
|
||||
const response = await window.apiCall(`${window.API}/daily-work-reports?date=${date}&view_all=true`);
|
||||
const response = await window.apiCall(`/daily-work-reports?date=${date}&view_all=true`);
|
||||
workData = Array.isArray(response) ? response : (response.data || []);
|
||||
console.log(`✅ 작업 데이터 ${workData.length}건 로드 완료`);
|
||||
return workData;
|
||||
@@ -261,48 +269,199 @@ function updateSummaryCard(element, value, unit) {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 작업 현황 표시 ========== //
|
||||
// ========== 작업 현황 표시 (작업자 중심) ========== //
|
||||
function displayWorkStatus() {
|
||||
if (!elements.workStatusContainer) return;
|
||||
|
||||
if (workData.length === 0) {
|
||||
// 모든 작업자 데이터 가져오기 (작업이 없는 작업자도 포함)
|
||||
const allWorkers = workersData || [];
|
||||
|
||||
if (allWorkers.length === 0) {
|
||||
elements.workStatusContainer.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">📭</div>
|
||||
<h3>작업 데이터가 없습니다</h3>
|
||||
<p>${selectedDate}에 등록된 작업이 없습니다.</p>
|
||||
<div class="empty-icon">👥</div>
|
||||
<h3>등록된 작업자가 없습니다</h3>
|
||||
<p>시스템에 작업자가 등록되어 있지 않습니다.</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// 프로젝트별 작업 현황 그룹화
|
||||
const projectGroups = groupWorkDataByProject();
|
||||
// 작업자별 상황 분석
|
||||
const workerStatusList = allWorkers.map(worker => {
|
||||
const todayWork = workData.filter(w => w.worker_id === worker.worker_id);
|
||||
const totalHours = todayWork.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0);
|
||||
|
||||
// 휴가/연차 제외한 실제 작업시간 계산
|
||||
const actualWorkHours = todayWork
|
||||
.filter(w => w.project_id !== 13) // 연차/휴무 프로젝트 제외
|
||||
.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0);
|
||||
|
||||
const hasError = todayWork.some(w => w.work_status_id === 2);
|
||||
|
||||
// 정규 작업과 에러 작업 건수 분리
|
||||
const regularWorkCount = todayWork.filter(w => w.project_id !== 13 && w.work_status_id !== 2).length;
|
||||
const errorWorkCount = todayWork.filter(w => w.project_id !== 13 && w.work_status_id === 2).length;
|
||||
|
||||
// 상태 판단 로직 (개선된 버전)
|
||||
let status = 'incomplete';
|
||||
let statusText = '미입력';
|
||||
let statusBadge = '미입력';
|
||||
let vacationType = null;
|
||||
|
||||
// 휴가 처리된 경우 확인 (프로젝트 ID 13 = "연차/휴무" 또는 설명에 휴가 키워드)
|
||||
const hasVacationRecord = todayWork.some(w =>
|
||||
w.project_id === 13 || // 연차/휴무 프로젝트
|
||||
(w.description && (
|
||||
w.description.includes('연차') ||
|
||||
w.description.includes('반차') ||
|
||||
w.description.includes('휴가')
|
||||
))
|
||||
);
|
||||
|
||||
// 연차/휴무 프로젝트의 시간 계산
|
||||
const vacationHours = todayWork
|
||||
.filter(w => w.project_id === 13)
|
||||
.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0);
|
||||
|
||||
if (totalHours > 12) {
|
||||
status = 'overtime-warning';
|
||||
statusText = '초과근무 확인필요';
|
||||
statusBadge = '확인필요';
|
||||
} else if (hasVacationRecord && vacationHours > 0) {
|
||||
// 연차/휴무 시간에 따른 상태 결정
|
||||
if (vacationHours === 8) {
|
||||
status = 'vacation-full';
|
||||
statusText = '연차';
|
||||
statusBadge = '연차';
|
||||
} else if (vacationHours === 6) {
|
||||
status = 'vacation-half-half';
|
||||
statusText = '조퇴';
|
||||
statusBadge = '조퇴';
|
||||
} else if (vacationHours === 4) {
|
||||
status = 'vacation-half';
|
||||
statusText = '반차';
|
||||
statusBadge = '반차';
|
||||
} else if (vacationHours === 2) {
|
||||
status = 'vacation-quarter';
|
||||
statusText = '반반차';
|
||||
statusBadge = '반반차';
|
||||
}
|
||||
} else if (totalHours > 8) {
|
||||
// 8시간 초과 - 연장근로
|
||||
status = 'overtime';
|
||||
statusText = '연장근로';
|
||||
statusBadge = '연장근로';
|
||||
} else if (totalHours === 8) {
|
||||
// 정확히 8시간 - 정시근로
|
||||
status = 'complete';
|
||||
statusText = '정시근로';
|
||||
statusBadge = '정시근로';
|
||||
} else if (totalHours > 0) {
|
||||
// 0시간 초과 8시간 미만 - 부분 입력
|
||||
status = 'partial';
|
||||
statusText = '부분 입력';
|
||||
statusBadge = '부분입력';
|
||||
|
||||
// 휴가 처리 필요 여부 판단
|
||||
if (totalHours === 0) {
|
||||
vacationType = 'full';
|
||||
} else if (totalHours === 4) {
|
||||
vacationType = 'half';
|
||||
} else if (totalHours === 6) {
|
||||
vacationType = 'half-half'; // 2시간 더 추가해서 조퇴 처리
|
||||
}
|
||||
} else {
|
||||
// 0시간 - 미입력
|
||||
status = 'incomplete';
|
||||
statusText = '미입력';
|
||||
statusBadge = '미입력';
|
||||
vacationType = 'full';
|
||||
}
|
||||
|
||||
return {
|
||||
...worker,
|
||||
todayWork,
|
||||
totalHours,
|
||||
actualWorkHours,
|
||||
regularWorkCount,
|
||||
errorWorkCount,
|
||||
hasError,
|
||||
status,
|
||||
statusText,
|
||||
statusBadge,
|
||||
vacationType
|
||||
};
|
||||
});
|
||||
|
||||
elements.workStatusContainer.innerHTML = `
|
||||
<div class="work-status-grid">
|
||||
${Object.entries(projectGroups).map(([projectName, works]) => `
|
||||
<div class="project-status-card">
|
||||
<div class="project-header">
|
||||
<h4 class="project-name">📁 ${projectName}</h4>
|
||||
<span class="work-count badge badge-primary">${works.length}건</span>
|
||||
</div>
|
||||
<div class="project-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">총 시간</span>
|
||||
<span class="stat-value">${works.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0).toFixed(1)}h</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">작업자</span>
|
||||
<span class="stat-value">${new Set(works.map(w => w.worker_id)).size}명</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">오류</span>
|
||||
<span class="stat-value ${works.filter(w => w.work_status_id === 2).length > 0 ? 'error' : ''}">${works.filter(w => w.work_status_id === 2).length}건</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="worker-status-list">
|
||||
<div class="worker-status-header">
|
||||
<div class="header-title">
|
||||
<h3>작업자별 현황</h3>
|
||||
<span class="header-date">${selectedDate}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
<div class="status-legend">
|
||||
<span class="legend-item legend-complete">정시근로</span>
|
||||
<span class="legend-item legend-overtime">연장근로</span>
|
||||
<span class="legend-item legend-vacation">휴가</span>
|
||||
<span class="legend-item legend-partial">부분입력</span>
|
||||
<span class="legend-item legend-incomplete">미입력</span>
|
||||
<span class="legend-item legend-error">오류</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="worker-status-rows">
|
||||
${workerStatusList.map(worker => `
|
||||
<div class="worker-status-row ${worker.status}" data-worker-id="${worker.worker_id}">
|
||||
<div class="worker-basic-info">
|
||||
<div class="worker-avatar">
|
||||
<span>${worker.worker_name.charAt(0)}</span>
|
||||
</div>
|
||||
<div class="worker-details">
|
||||
<h4 class="worker-name">${worker.worker_name}</h4>
|
||||
<p class="worker-job">${worker.job_type || '작업자'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="worker-status-indicator">
|
||||
<span class="status-badge status-${worker.status}">${worker.statusBadge}</span>
|
||||
</div>
|
||||
|
||||
<div class="worker-stats-inline">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">작업시간</span>
|
||||
<span class="stat-value ${worker.actualWorkHours > 12 ? 'warning' : ''}">${worker.actualWorkHours.toFixed(1)}h</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">정규</span>
|
||||
<span class="stat-value">${worker.regularWorkCount}건</span>
|
||||
</div>
|
||||
${worker.errorWorkCount > 0 ? `
|
||||
<div class="stat-item error">
|
||||
<span class="stat-label">에러</span>
|
||||
<span class="stat-value">${worker.errorWorkCount}건</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<div class="worker-actions-inline">
|
||||
<button class="btn btn-sm btn-primary worker-edit-btn" onclick="openWorkerModal(${worker.worker_id}, '${worker.worker_name}')">
|
||||
작업입력
|
||||
</button>
|
||||
${worker.vacationType ? `
|
||||
<button class="btn btn-sm btn-secondary vacation-btn" onclick="handleVacation(${worker.worker_id}, '${worker.vacationType}')">
|
||||
${worker.vacationType === 'full' ? '연차처리' : worker.vacationType === 'half' ? '반차처리' : '반반차처리'}
|
||||
</button>
|
||||
` : ''}
|
||||
${worker.status === 'overtime-warning' ? `
|
||||
<button class="btn btn-sm btn-warning confirm-overtime-btn" onclick="confirmOvertime(${worker.worker_id})">
|
||||
정상확인
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -347,8 +506,18 @@ function displayWorkersAsCards(workers) {
|
||||
${workers.map(worker => {
|
||||
const todayWork = workData.filter(w => w.worker_id === worker.worker_id);
|
||||
const totalHours = todayWork.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0);
|
||||
|
||||
// 휴가/연차 제외한 실제 작업시간 계산
|
||||
const actualWorkHours = todayWork
|
||||
.filter(w => w.project_id !== 13) // 연차/휴무 프로젝트 제외
|
||||
.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0);
|
||||
|
||||
const hasError = todayWork.some(w => w.work_status_id === 2);
|
||||
|
||||
// 정규 작업과 에러 작업 건수 분리
|
||||
const regularWorkCount = todayWork.filter(w => w.project_id !== 13 && w.work_status_id !== 2).length;
|
||||
const errorWorkCount = todayWork.filter(w => w.project_id !== 13 && w.work_status_id === 2).length;
|
||||
|
||||
return `
|
||||
<div class="worker-card card">
|
||||
<div class="card-body">
|
||||
@@ -366,17 +535,17 @@ function displayWorkersAsCards(workers) {
|
||||
</div>
|
||||
<div class="worker-stats">
|
||||
<div class="stat">
|
||||
<span class="stat-label">오늘 작업</span>
|
||||
<span class="stat-value">${todayWork.length}건</span>
|
||||
<span class="stat-label">작업시간</span>
|
||||
<span class="stat-value">${actualWorkHours.toFixed(1)}h</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-label">작업 시간</span>
|
||||
<span class="stat-value">${totalHours.toFixed(1)}h</span>
|
||||
<span class="stat-label">정규</span>
|
||||
<span class="stat-value">${regularWorkCount}건</span>
|
||||
</div>
|
||||
${hasError ? `
|
||||
${errorWorkCount > 0 ? `
|
||||
<div class="stat error">
|
||||
<span class="stat-label">오류</span>
|
||||
<span class="stat-value">⚠️</span>
|
||||
<span class="stat-label">에러</span>
|
||||
<span class="stat-value">${errorWorkCount}건</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
@@ -531,8 +700,566 @@ function showToast(message, type = 'info', duration = 3000) {
|
||||
}, duration);
|
||||
}
|
||||
|
||||
// ========== 작업자 관련 액션 함수들 ========== //
|
||||
function openWorkerModal(workerId, workerName) {
|
||||
console.log(`📝 ${workerName}(ID: ${workerId}) 작업 보고서 모달 열기`);
|
||||
|
||||
// 모달 데이터 설정
|
||||
currentModalWorker = {
|
||||
id: workerId,
|
||||
name: workerName,
|
||||
date: selectedDate
|
||||
};
|
||||
|
||||
// 모달 표시
|
||||
showWorkerModal();
|
||||
}
|
||||
|
||||
function handleVacation(workerId, vacationType) {
|
||||
console.log(`🏖️ 작업자 ${workerId} 휴가 처리: ${vacationType}`);
|
||||
|
||||
const vacationNames = {
|
||||
'full': '연차',
|
||||
'half': '반차',
|
||||
'half-half': '반반차'
|
||||
};
|
||||
|
||||
const vacationHours = {
|
||||
'full': 8,
|
||||
'half': 4,
|
||||
'half-half': 2
|
||||
};
|
||||
|
||||
if (confirm(`${vacationNames[vacationType]} 처리하시겠습니까?\n(${vacationHours[vacationType]}시간으로 자동 입력됩니다)`)) {
|
||||
// 휴가 처리 API 호출
|
||||
processVacation(workerId, vacationType, vacationHours[vacationType]);
|
||||
}
|
||||
}
|
||||
|
||||
async function processVacation(workerId, vacationType, hours) {
|
||||
try {
|
||||
showToast(`휴가 처리 중...`, 'info');
|
||||
|
||||
// 휴가용 작업 보고서 생성 (특별한 작업 유형으로)
|
||||
const vacationReport = {
|
||||
report_date: selectedDate,
|
||||
worker_id: workerId,
|
||||
project_id: 1, // 기본 프로젝트 (휴가용)
|
||||
work_type_id: 999, // 휴가 전용 작업 유형 (DB에 추가 필요)
|
||||
work_status_id: 1, // 정상 상태
|
||||
error_type_id: null,
|
||||
work_hours: hours,
|
||||
created_by: currentUser?.user_id || 1
|
||||
};
|
||||
|
||||
const response = await window.apiCall(`${window.API}/daily-work-reports`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(vacationReport)
|
||||
});
|
||||
|
||||
showToast(`휴가 처리가 완료되었습니다.`, 'success');
|
||||
await loadDashboardData(); // 데이터 새로고침
|
||||
|
||||
} catch (error) {
|
||||
console.error('휴가 처리 오류:', error);
|
||||
showToast(`휴가 처리 중 오류가 발생했습니다: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function confirmOvertime(workerId) {
|
||||
console.log(`⚠️ 작업자 ${workerId} 초과근무 확인`);
|
||||
|
||||
if (confirm('12시간을 초과한 작업시간이 정상적인 입력인지 확인하시겠습니까?')) {
|
||||
// 초과근무 확인 처리
|
||||
processOvertimeConfirmation(workerId);
|
||||
}
|
||||
}
|
||||
|
||||
async function processOvertimeConfirmation(workerId) {
|
||||
try {
|
||||
showToast('초과근무 승인 처리 중...', 'info');
|
||||
|
||||
// 새로운 근태 관리 API 사용
|
||||
const overtimeData = {
|
||||
worker_id: workerId,
|
||||
date: selectedDate
|
||||
};
|
||||
|
||||
const response = await window.apiCall('/attendance/overtime/approve', 'POST', overtimeData);
|
||||
|
||||
if (response.success) {
|
||||
showToast('초과근무가 정상으로 승인되었습니다.', 'success');
|
||||
await loadDashboardData(); // 데이터 새로고침
|
||||
} else {
|
||||
throw new Error(response.message || '초과근무 승인에 실패했습니다.');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('초과근무 승인 오류:', error);
|
||||
showToast(`초과근무 승인 중 오류가 발생했습니다: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 모달 시스템 ========== //
|
||||
function showWorkerModal() {
|
||||
// 모달이 없으면 생성
|
||||
if (!document.getElementById('workerModal')) {
|
||||
createWorkerModal();
|
||||
}
|
||||
|
||||
// 모달 데이터 로드 및 표시
|
||||
loadModalData();
|
||||
document.getElementById('workerModal').style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden'; // 배경 스크롤 방지
|
||||
}
|
||||
|
||||
function hideWorkerModal() {
|
||||
document.getElementById('workerModal').style.display = 'none';
|
||||
document.body.style.overflow = 'auto'; // 배경 스크롤 복원
|
||||
resetModalForm();
|
||||
}
|
||||
|
||||
function createWorkerModal() {
|
||||
const modalHTML = `
|
||||
<div id="workerModal" class="modal-overlay">
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<h2 id="modalTitle">작업자 보고서</h2>
|
||||
<button class="modal-close-btn" onclick="hideWorkerModal()">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<!-- 작업자 정보 -->
|
||||
<div class="modal-worker-info">
|
||||
<div class="modal-worker-avatar">
|
||||
<span id="modalWorkerInitial">작</span>
|
||||
</div>
|
||||
<div class="modal-worker-details">
|
||||
<h3 id="modalWorkerName">작업자명</h3>
|
||||
<p id="modalWorkerDate">날짜</p>
|
||||
</div>
|
||||
<div class="modal-worker-summary">
|
||||
<div class="modal-stat">
|
||||
<span class="modal-stat-label">총 시간</span>
|
||||
<span class="modal-stat-value" id="modalTotalHours">0h</span>
|
||||
</div>
|
||||
<div class="modal-stat">
|
||||
<span class="modal-stat-label">작업 건수</span>
|
||||
<span class="modal-stat-value" id="modalWorkCount">0건</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 기존 작업 목록 -->
|
||||
<div class="modal-section">
|
||||
<div class="modal-section-header">
|
||||
<h4>기존 작업 목록</h4>
|
||||
<button class="btn btn-sm btn-primary" id="modalAddWorkBtn">새 작업 추가</button>
|
||||
</div>
|
||||
<div id="modalExistingWork" class="modal-existing-work">
|
||||
<!-- 기존 작업들이 여기에 표시됩니다 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 새 작업 추가 폼 -->
|
||||
<div class="modal-section" id="modalNewWorkSection" style="display: none;">
|
||||
<div class="modal-section-header">
|
||||
<h4>새 작업 추가</h4>
|
||||
<button class="btn btn-sm btn-secondary" id="modalCancelWorkBtn">취소</button>
|
||||
</div>
|
||||
<div class="modal-work-form">
|
||||
<div class="modal-form-row">
|
||||
<div class="modal-form-group">
|
||||
<label>프로젝트</label>
|
||||
<select id="modalProjectSelect" class="modal-select">
|
||||
<option value="">프로젝트 선택</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="modal-form-group">
|
||||
<label>작업 유형</label>
|
||||
<select id="modalWorkTypeSelect" class="modal-select">
|
||||
<option value="">작업 유형 선택</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-form-group">
|
||||
<label>업무 상태</label>
|
||||
<select id="modalWorkStatusSelect" class="modal-select">
|
||||
<option value="">업무 상태 선택</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="modal-form-group" id="modalErrorTypeGroup" style="display: none;">
|
||||
<label>에러 유형</label>
|
||||
<select id="modalErrorTypeSelect" class="modal-select">
|
||||
<option value="">에러 유형 선택</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="modal-form-group">
|
||||
<label>작업 시간</label>
|
||||
<input type="number" id="modalWorkHours" class="modal-input" step="0.25" min="0.25" max="24" value="1.00">
|
||||
<div class="modal-quick-time">
|
||||
<button type="button" class="modal-time-btn" data-hours="0.5">0.5h</button>
|
||||
<button type="button" class="modal-time-btn" data-hours="1">1h</button>
|
||||
<button type="button" class="modal-time-btn" data-hours="2">2h</button>
|
||||
<button type="button" class="modal-time-btn" data-hours="4">4h</button>
|
||||
<button type="button" class="modal-time-btn" data-hours="8">8h</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success modal-save-btn" id="modalSaveWorkBtn">작업 저장</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 휴가 처리 -->
|
||||
<div class="modal-section">
|
||||
<div class="modal-section-header">
|
||||
<h4>휴가 처리</h4>
|
||||
</div>
|
||||
<div class="modal-vacation-buttons">
|
||||
<button class="btn btn-warning modal-vacation-btn" data-type="full">연차 (8시간)</button>
|
||||
<button class="btn btn-warning modal-vacation-btn" data-type="half-half">반반차 (6시간)</button>
|
||||
<button class="btn btn-warning modal-vacation-btn" data-type="half">반차 (4시간)</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="hideWorkerModal()">닫기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
||||
setupModalEventListeners();
|
||||
}
|
||||
|
||||
function setupModalEventListeners() {
|
||||
// 모달 외부 클릭 시 닫기
|
||||
document.getElementById('workerModal').addEventListener('click', (e) => {
|
||||
if (e.target.id === 'workerModal') {
|
||||
hideWorkerModal();
|
||||
}
|
||||
});
|
||||
|
||||
// 새 작업 추가/취소 버튼
|
||||
document.getElementById('modalAddWorkBtn').addEventListener('click', showModalNewWorkForm);
|
||||
document.getElementById('modalCancelWorkBtn').addEventListener('click', hideModalNewWorkForm);
|
||||
document.getElementById('modalSaveWorkBtn').addEventListener('click', saveModalNewWork);
|
||||
|
||||
// 업무 상태 변경 시 에러 유형 토글
|
||||
document.getElementById('modalWorkStatusSelect').addEventListener('change', toggleModalErrorType);
|
||||
|
||||
// 빠른 시간 버튼
|
||||
document.querySelectorAll('.modal-time-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
document.getElementById('modalWorkHours').value = e.target.dataset.hours;
|
||||
});
|
||||
});
|
||||
|
||||
// 휴가 처리 버튼
|
||||
document.querySelectorAll('.modal-vacation-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const vacationType = e.target.dataset.type;
|
||||
handleModalVacation(vacationType);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function loadModalData() {
|
||||
if (!currentModalWorker) return;
|
||||
|
||||
try {
|
||||
// 모달 헤더 업데이트
|
||||
document.getElementById('modalTitle').textContent = `${currentModalWorker.name} 작업 보고서`;
|
||||
document.getElementById('modalWorkerName').textContent = currentModalWorker.name;
|
||||
document.getElementById('modalWorkerDate').textContent = currentModalWorker.date;
|
||||
document.getElementById('modalWorkerInitial').textContent = currentModalWorker.name.charAt(0);
|
||||
|
||||
// 병렬로 데이터 로드
|
||||
await Promise.all([
|
||||
loadModalExistingWork(),
|
||||
loadModalDropdownData()
|
||||
]);
|
||||
|
||||
// UI 업데이트
|
||||
updateModalSummary();
|
||||
renderModalExistingWork();
|
||||
populateModalDropdowns();
|
||||
|
||||
} catch (error) {
|
||||
console.error('모달 데이터 로드 오류:', error);
|
||||
showToast('데이터 로드 중 오류가 발생했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadModalExistingWork() {
|
||||
try {
|
||||
const response = await window.apiCall(`${window.API}/daily-work-reports?date=${currentModalWorker.date}&worker_id=${currentModalWorker.id}`);
|
||||
modalExistingWork = Array.isArray(response) ? response : (response.data || []);
|
||||
} catch (error) {
|
||||
console.error('기존 작업 로드 오류:', error);
|
||||
modalExistingWork = [];
|
||||
}
|
||||
}
|
||||
|
||||
async function loadModalDropdownData() {
|
||||
try {
|
||||
const [projectsRes, workTypesRes, workStatusRes, errorTypesRes] = await Promise.all([
|
||||
window.apiCall(`${window.API}/projects`),
|
||||
window.apiCall(`${window.API}/daily-work-reports/work-types`),
|
||||
window.apiCall(`${window.API}/daily-work-reports/work-status-types`),
|
||||
window.apiCall(`${window.API}/daily-work-reports/error-types`)
|
||||
]);
|
||||
|
||||
modalProjects = Array.isArray(projectsRes) ? projectsRes : (projectsRes.data || []);
|
||||
modalWorkTypes = Array.isArray(workTypesRes) ? workTypesRes : (workTypesRes.data || []);
|
||||
modalWorkStatusTypes = Array.isArray(workStatusRes) ? workStatusRes : (workStatusRes.data || []);
|
||||
modalErrorTypes = Array.isArray(errorTypesRes) ? errorTypesRes : (errorTypesRes.data || []);
|
||||
} catch (error) {
|
||||
console.error('드롭다운 데이터 로드 오류:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function updateModalSummary() {
|
||||
const totalHours = modalExistingWork.reduce((sum, work) => sum + parseFloat(work.work_hours || 0), 0);
|
||||
const workCount = modalExistingWork.length;
|
||||
|
||||
document.getElementById('modalTotalHours').textContent = `${totalHours.toFixed(1)}h`;
|
||||
document.getElementById('modalWorkCount').textContent = `${workCount}건`;
|
||||
|
||||
// 12시간 초과 경고
|
||||
if (totalHours > 12) {
|
||||
document.getElementById('modalTotalHours').classList.add('warning');
|
||||
} else {
|
||||
document.getElementById('modalTotalHours').classList.remove('warning');
|
||||
}
|
||||
}
|
||||
|
||||
function renderModalExistingWork() {
|
||||
const container = document.getElementById('modalExistingWork');
|
||||
|
||||
if (modalExistingWork.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="modal-empty-state">
|
||||
<div class="modal-empty-icon">—</div>
|
||||
<p>등록된 작업이 없습니다.</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = modalExistingWork.map(work => `
|
||||
<div class="modal-work-item">
|
||||
<div class="modal-work-info">
|
||||
<h5>${work.project_name || '미지정 프로젝트'}</h5>
|
||||
<p>${work.work_type_name || '미지정 작업'}</p>
|
||||
${work.work_status_id === 2 && work.error_type_name ? `
|
||||
<span class="modal-error-badge">${work.error_type_name}</span>
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="modal-work-actions">
|
||||
<span class="modal-work-hours">${work.work_hours}h</span>
|
||||
<button class="btn btn-xs btn-danger" onclick="deleteModalWork(${work.id})">삭제</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function populateModalDropdowns() {
|
||||
// 프로젝트 드롭다운
|
||||
const projectSelect = document.getElementById('modalProjectSelect');
|
||||
projectSelect.innerHTML = '<option value="">프로젝트 선택</option>';
|
||||
modalProjects.forEach(project => {
|
||||
projectSelect.innerHTML += `<option value="${project.project_id}">${project.project_name}</option>`;
|
||||
});
|
||||
|
||||
// 작업 유형 드롭다운
|
||||
const workTypeSelect = document.getElementById('modalWorkTypeSelect');
|
||||
workTypeSelect.innerHTML = '<option value="">작업 유형 선택</option>';
|
||||
modalWorkTypes.forEach(type => {
|
||||
workTypeSelect.innerHTML += `<option value="${type.id}">${type.name}</option>`;
|
||||
});
|
||||
|
||||
// 작업 상태 드롭다운
|
||||
const workStatusSelect = document.getElementById('modalWorkStatusSelect');
|
||||
workStatusSelect.innerHTML = '<option value="">업무 상태 선택</option>';
|
||||
modalWorkStatusTypes.forEach(status => {
|
||||
workStatusSelect.innerHTML += `<option value="${status.id}">${status.name}</option>`;
|
||||
});
|
||||
|
||||
// 에러 유형 드롭다운
|
||||
const errorTypeSelect = document.getElementById('modalErrorTypeSelect');
|
||||
errorTypeSelect.innerHTML = '<option value="">에러 유형 선택</option>';
|
||||
modalErrorTypes.forEach(error => {
|
||||
errorTypeSelect.innerHTML += `<option value="${error.id}">${error.name}</option>`;
|
||||
});
|
||||
}
|
||||
|
||||
function showModalNewWorkForm() {
|
||||
document.getElementById('modalNewWorkSection').style.display = 'block';
|
||||
document.getElementById('modalAddWorkBtn').style.display = 'none';
|
||||
}
|
||||
|
||||
function hideModalNewWorkForm() {
|
||||
document.getElementById('modalNewWorkSection').style.display = 'none';
|
||||
document.getElementById('modalAddWorkBtn').style.display = 'block';
|
||||
resetModalForm();
|
||||
}
|
||||
|
||||
function resetModalForm() {
|
||||
document.getElementById('modalProjectSelect').value = '';
|
||||
document.getElementById('modalWorkTypeSelect').value = '';
|
||||
document.getElementById('modalWorkStatusSelect').value = '';
|
||||
document.getElementById('modalErrorTypeSelect').value = '';
|
||||
document.getElementById('modalWorkHours').value = '1.00';
|
||||
document.getElementById('modalErrorTypeGroup').style.display = 'none';
|
||||
}
|
||||
|
||||
function toggleModalErrorType() {
|
||||
const workStatusSelect = document.getElementById('modalWorkStatusSelect');
|
||||
const errorTypeGroup = document.getElementById('modalErrorTypeGroup');
|
||||
|
||||
if (workStatusSelect.value === '2') { // 에러 상태
|
||||
errorTypeGroup.style.display = 'block';
|
||||
} else {
|
||||
errorTypeGroup.style.display = 'none';
|
||||
document.getElementById('modalErrorTypeSelect').value = '';
|
||||
}
|
||||
}
|
||||
|
||||
async function saveModalNewWork() {
|
||||
try {
|
||||
const projectId = document.getElementById('modalProjectSelect').value;
|
||||
const workTypeId = document.getElementById('modalWorkTypeSelect').value;
|
||||
const workStatusId = document.getElementById('modalWorkStatusSelect').value;
|
||||
const errorTypeId = document.getElementById('modalErrorTypeSelect').value;
|
||||
const workHours = document.getElementById('modalWorkHours').value;
|
||||
|
||||
// 유효성 검사
|
||||
if (!projectId || !workTypeId || !workStatusId || !workHours) {
|
||||
showToast('모든 필수 필드를 입력해주세요.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (workStatusId === '2' && !errorTypeId) {
|
||||
showToast('에러 상태일 때는 에러 유형을 선택해야 합니다.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const workData = {
|
||||
report_date: currentModalWorker.date,
|
||||
worker_id: currentModalWorker.id,
|
||||
project_id: parseInt(projectId),
|
||||
work_type_id: parseInt(workTypeId),
|
||||
work_status_id: parseInt(workStatusId),
|
||||
error_type_id: workStatusId === '2' ? parseInt(errorTypeId) : null,
|
||||
work_hours: parseFloat(workHours),
|
||||
created_by: currentUser?.user_id || 1
|
||||
};
|
||||
|
||||
await window.apiCall(`${window.API}/daily-work-reports`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(workData)
|
||||
});
|
||||
|
||||
showToast('작업이 성공적으로 저장되었습니다.', 'success');
|
||||
|
||||
// 데이터 새로고침
|
||||
await loadModalExistingWork();
|
||||
updateModalSummary();
|
||||
renderModalExistingWork();
|
||||
hideModalNewWorkForm();
|
||||
|
||||
// 대시보드 데이터도 새로고침
|
||||
await loadDashboardData();
|
||||
|
||||
} catch (error) {
|
||||
console.error('작업 저장 오류:', error);
|
||||
showToast(`작업 저장 중 오류가 발생했습니다: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteModalWork(workId) {
|
||||
if (!confirm('이 작업을 삭제하시겠습니까?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await window.apiCall(`${window.API}/daily-work-reports/${workId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
showToast('작업이 성공적으로 삭제되었습니다.', 'success');
|
||||
|
||||
// 데이터 새로고침
|
||||
await loadModalExistingWork();
|
||||
updateModalSummary();
|
||||
renderModalExistingWork();
|
||||
|
||||
// 대시보드 데이터도 새로고침
|
||||
await loadDashboardData();
|
||||
|
||||
} catch (error) {
|
||||
console.error('작업 삭제 오류:', error);
|
||||
showToast(`작업 삭제 중 오류가 발생했습니다: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleModalVacation(vacationType) {
|
||||
const vacationTypeMap = {
|
||||
'full': { code: 'ANNUAL_FULL', name: '연차', hours: 8 },
|
||||
'half': { code: 'ANNUAL_HALF', name: '반차', hours: 4 },
|
||||
'half-half': { code: 'ANNUAL_QUARTER', name: '반반차', hours: 2 }
|
||||
};
|
||||
|
||||
const vacation = vacationTypeMap[vacationType];
|
||||
if (!vacation) return;
|
||||
|
||||
if (!confirm(`${vacation.name} 처리하시겠습니까?\n(${vacation.hours}시간으로 자동 입력됩니다)`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 새로운 근태 관리 API 사용
|
||||
const vacationData = {
|
||||
worker_id: currentModalWorker.id,
|
||||
date: currentModalWorker.date,
|
||||
vacation_type: vacation.code
|
||||
};
|
||||
|
||||
const response = await window.apiCall('/attendance/vacation', 'POST', vacationData);
|
||||
|
||||
if (response.success) {
|
||||
showToast(`${vacation.name} 처리가 완료되었습니다.`, 'success');
|
||||
|
||||
// 데이터 새로고침
|
||||
await loadModalExistingWork();
|
||||
updateModalSummary();
|
||||
renderModalExistingWork();
|
||||
|
||||
// 대시보드 데이터도 새로고침
|
||||
await loadDashboardData();
|
||||
} else {
|
||||
throw new Error(response.message || '휴가 처리에 실패했습니다.');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('휴가 처리 오류:', error);
|
||||
showToast(`휴가 처리 중 오류가 발생했습니다: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 전역 함수 (HTML에서 호출) ========== //
|
||||
window.loadDashboardData = loadDashboardData;
|
||||
window.showToast = showToast;
|
||||
window.updateSummaryCards = updateSummaryCards;
|
||||
window.displayWorkers = displayWorkers;
|
||||
window.openWorkerModal = openWorkerModal;
|
||||
window.hideWorkerModal = hideWorkerModal;
|
||||
window.deleteModalWork = deleteModalWork;
|
||||
window.handleVacation = handleVacation;
|
||||
window.confirmOvertime = confirmOvertime;
|
||||
|
||||
1108
web-ui/js/work-report-calendar.js
Normal file
1108
web-ui/js/work-report-calendar.js
Normal file
File diff suppressed because it is too large
Load Diff
512
web-ui/js/worker-individual-report.js
Normal file
512
web-ui/js/worker-individual-report.js
Normal file
@@ -0,0 +1,512 @@
|
||||
// worker-individual-report.js - 작업자별 개별 보고서 관리
|
||||
|
||||
// 전역 변수
|
||||
let currentWorkerId = null;
|
||||
let currentWorkerName = '';
|
||||
let selectedDate = '';
|
||||
let currentUser = null;
|
||||
let workTypes = [];
|
||||
let workStatusTypes = [];
|
||||
let errorTypes = [];
|
||||
let projects = [];
|
||||
let existingWork = [];
|
||||
|
||||
// URL 파라미터에서 정보 추출
|
||||
function getUrlParams() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return {
|
||||
worker_id: urlParams.get('worker_id'),
|
||||
worker_name: decodeURIComponent(urlParams.get('worker_name') || ''),
|
||||
date: urlParams.get('date') || new Date().toISOString().split('T')[0]
|
||||
};
|
||||
}
|
||||
|
||||
// 현재 로그인한 사용자 정보 가져오기
|
||||
function getCurrentUser() {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return null;
|
||||
|
||||
const payloadBase64 = token.split('.')[1];
|
||||
if (payloadBase64) {
|
||||
const payload = JSON.parse(atob(payloadBase64));
|
||||
return payload;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('토큰에서 사용자 정보 추출 실패:', error);
|
||||
}
|
||||
|
||||
try {
|
||||
const userInfo = localStorage.getItem('user');
|
||||
if (userInfo) {
|
||||
return JSON.parse(userInfo);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('localStorage에서 사용자 정보 파싱 실패:', error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 메시지 표시 함수
|
||||
function showMessage(msg, type = 'info') {
|
||||
const container = document.getElementById('message-container');
|
||||
if (container) {
|
||||
container.innerHTML = `<div class="message ${type}">${msg}</div>`;
|
||||
setTimeout(() => {
|
||||
container.innerHTML = '';
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
// 페이지 초기화
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
// API 함수가 로드될 때까지 기다림
|
||||
let retryCount = 0;
|
||||
const maxRetries = 50;
|
||||
|
||||
while (!window.apiCall && retryCount < maxRetries) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
retryCount++;
|
||||
}
|
||||
|
||||
if (!window.apiCall) {
|
||||
console.error('❌ API 함수를 로드할 수 없습니다.');
|
||||
showMessage('시스템을 초기화할 수 없습니다. 페이지를 새로고침해주세요.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await initializePage();
|
||||
} catch (error) {
|
||||
console.error('페이지 초기화 오류:', error);
|
||||
showMessage('페이지를 불러오는 중 오류가 발생했습니다.', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
async function initializePage() {
|
||||
console.log('🚀 개별 작업 보고서 페이지 초기화 시작');
|
||||
|
||||
// URL 파라미터 추출
|
||||
const params = getUrlParams();
|
||||
currentWorkerId = parseInt(params.worker_id);
|
||||
currentWorkerName = params.worker_name;
|
||||
selectedDate = params.date;
|
||||
|
||||
// 사용자 정보 설정
|
||||
currentUser = getCurrentUser();
|
||||
|
||||
if (!currentWorkerId || !currentWorkerName) {
|
||||
showMessage('잘못된 접근입니다. 작업자 정보가 없습니다.', 'error');
|
||||
setTimeout(() => {
|
||||
window.history.back();
|
||||
}, 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
// 페이지 제목 설정
|
||||
updatePageHeader();
|
||||
|
||||
// 이벤트 리스너 설정
|
||||
setupEventListeners();
|
||||
|
||||
// 초기 데이터 로드
|
||||
await loadInitialData();
|
||||
|
||||
console.log('✅ 개별 작업 보고서 페이지 초기화 완료');
|
||||
}
|
||||
|
||||
function updatePageHeader() {
|
||||
document.getElementById('pageTitle').textContent = `👤 ${currentWorkerName} 작업 보고서`;
|
||||
document.getElementById('pageSubtitle').textContent = `${selectedDate} 작업 내용을 관리합니다.`;
|
||||
|
||||
// 작업자 정보 카드 업데이트
|
||||
document.getElementById('workerInitial').textContent = currentWorkerName.charAt(0);
|
||||
document.getElementById('workerName').textContent = currentWorkerName;
|
||||
document.getElementById('selectedDate').textContent = selectedDate;
|
||||
}
|
||||
|
||||
function setupEventListeners() {
|
||||
// 새 작업 추가 버튼
|
||||
document.getElementById('addNewWorkBtn').addEventListener('click', showNewWorkForm);
|
||||
document.getElementById('cancelNewWorkBtn').addEventListener('click', hideNewWorkForm);
|
||||
document.getElementById('saveNewWorkBtn').addEventListener('click', saveNewWork);
|
||||
|
||||
// 업무 상태 변경 시 에러 유형 섹션 토글
|
||||
document.getElementById('newWorkStatusSelect').addEventListener('change', toggleErrorTypeSection);
|
||||
|
||||
// 빠른 시간 버튼
|
||||
document.querySelectorAll('.quick-time-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
document.getElementById('newWorkHours').value = e.target.dataset.hours;
|
||||
});
|
||||
});
|
||||
|
||||
// 휴가 처리 버튼들
|
||||
document.querySelectorAll('.vacation-process-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const vacationType = e.target.dataset.type;
|
||||
handleVacationProcess(vacationType);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function loadInitialData() {
|
||||
try {
|
||||
showMessage('데이터를 불러오는 중...', 'loading');
|
||||
|
||||
// 병렬로 데이터 로드
|
||||
await Promise.all([
|
||||
loadWorkerInfo(),
|
||||
loadExistingWork(),
|
||||
loadProjects(),
|
||||
loadWorkTypes(),
|
||||
loadWorkStatusTypes(),
|
||||
loadErrorTypes()
|
||||
]);
|
||||
|
||||
// UI 업데이트
|
||||
updateWorkerSummary();
|
||||
renderExistingWork();
|
||||
populateDropdowns();
|
||||
|
||||
showMessage('데이터 로드 완료', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('초기 데이터 로드 실패:', error);
|
||||
showMessage('데이터 로드 중 오류가 발생했습니다: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadWorkerInfo() {
|
||||
try {
|
||||
const response = await window.apiCall(`${window.API}/workers/${currentWorkerId}`);
|
||||
const worker = response.data || response;
|
||||
document.getElementById('workerJob').textContent = worker.job_type || '작업자';
|
||||
} catch (error) {
|
||||
console.error('작업자 정보 로드 오류:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadExistingWork() {
|
||||
try {
|
||||
const response = await window.apiCall(`${window.API}/daily-work-reports?date=${selectedDate}&worker_id=${currentWorkerId}`);
|
||||
existingWork = Array.isArray(response) ? response : (response.data || []);
|
||||
console.log(`✅ 기존 작업 ${existingWork.length}건 로드 완료`);
|
||||
} catch (error) {
|
||||
console.error('기존 작업 로드 오류:', error);
|
||||
existingWork = [];
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProjects() {
|
||||
try {
|
||||
const response = await window.apiCall(`${window.API}/projects`);
|
||||
projects = Array.isArray(response) ? response : (response.data || []);
|
||||
} catch (error) {
|
||||
console.error('프로젝트 로드 오류:', error);
|
||||
projects = [];
|
||||
}
|
||||
}
|
||||
|
||||
async function loadWorkTypes() {
|
||||
try {
|
||||
const response = await window.apiCall(`${window.API}/daily-work-reports/work-types`);
|
||||
workTypes = Array.isArray(response) ? response : (response.data || []);
|
||||
} catch (error) {
|
||||
console.error('작업 유형 로드 오류:', error);
|
||||
workTypes = [];
|
||||
}
|
||||
}
|
||||
|
||||
async function loadWorkStatusTypes() {
|
||||
try {
|
||||
const response = await window.apiCall(`${window.API}/daily-work-reports/work-status-types`);
|
||||
workStatusTypes = Array.isArray(response) ? response : (response.data || []);
|
||||
} catch (error) {
|
||||
console.error('작업 상태 유형 로드 오류:', error);
|
||||
workStatusTypes = [];
|
||||
}
|
||||
}
|
||||
|
||||
async function loadErrorTypes() {
|
||||
try {
|
||||
const response = await window.apiCall(`${window.API}/daily-work-reports/error-types`);
|
||||
errorTypes = Array.isArray(response) ? response : (response.data || []);
|
||||
} catch (error) {
|
||||
console.error('에러 유형 로드 오류:', error);
|
||||
errorTypes = [];
|
||||
}
|
||||
}
|
||||
|
||||
function updateWorkerSummary() {
|
||||
const totalHours = existingWork.reduce((sum, work) => sum + parseFloat(work.work_hours || 0), 0);
|
||||
const workCount = existingWork.length;
|
||||
|
||||
document.getElementById('totalHours').textContent = `${totalHours.toFixed(1)}h`;
|
||||
document.getElementById('workCount').textContent = `${workCount}건`;
|
||||
|
||||
// 12시간 초과 경고
|
||||
if (totalHours > 12) {
|
||||
document.getElementById('totalHours').classList.add('warning');
|
||||
showMessage(`⚠️ 총 작업시간이 ${totalHours.toFixed(1)}시간으로 12시간을 초과했습니다.`, 'warning');
|
||||
}
|
||||
}
|
||||
|
||||
function renderExistingWork() {
|
||||
const container = document.getElementById('existingWorkList');
|
||||
|
||||
if (existingWork.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">📭</div>
|
||||
<h3>등록된 작업이 없습니다</h3>
|
||||
<p>${selectedDate}에 ${currentWorkerName}님의 작업이 등록되지 않았습니다.</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = existingWork.map(work => `
|
||||
<div class="existing-work-item" data-work-id="${work.id}">
|
||||
<div class="work-item-header">
|
||||
<div class="work-item-info">
|
||||
<h4>${work.project_name || '미지정 프로젝트'}</h4>
|
||||
<p>${work.work_type_name || '미지정 작업'}</p>
|
||||
</div>
|
||||
<div class="work-item-status">
|
||||
<span class="status-badge ${work.work_status_id === 2 ? 'error' : 'normal'}">
|
||||
${work.work_status_name || '정상'}
|
||||
</span>
|
||||
<span class="work-hours">${work.work_hours}h</span>
|
||||
</div>
|
||||
</div>
|
||||
${work.work_status_id === 2 && work.error_type_name ? `
|
||||
<div class="work-item-error">
|
||||
<span class="error-label">오류:</span>
|
||||
<span class="error-type">${work.error_type_name}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="work-item-actions">
|
||||
<button class="btn btn-sm btn-primary" onclick="editWork(${work.id})">
|
||||
✏️ 수정
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteWork(${work.id})">
|
||||
🗑️ 삭제
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function populateDropdowns() {
|
||||
// 프로젝트 드롭다운
|
||||
const projectSelect = document.getElementById('newProjectSelect');
|
||||
projectSelect.innerHTML = '<option value="">프로젝트를 선택하세요</option>';
|
||||
projects.forEach(project => {
|
||||
const option = document.createElement('option');
|
||||
option.value = project.project_id;
|
||||
option.textContent = project.project_name;
|
||||
projectSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// 작업 유형 드롭다운
|
||||
const workTypeSelect = document.getElementById('newWorkTypeSelect');
|
||||
workTypeSelect.innerHTML = '<option value="">작업 유형을 선택하세요</option>';
|
||||
workTypes.forEach(type => {
|
||||
const option = document.createElement('option');
|
||||
option.value = type.id;
|
||||
option.textContent = type.name;
|
||||
workTypeSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// 작업 상태 드롭다운
|
||||
const workStatusSelect = document.getElementById('newWorkStatusSelect');
|
||||
workStatusSelect.innerHTML = '<option value="">업무 상태를 선택하세요</option>';
|
||||
workStatusTypes.forEach(status => {
|
||||
const option = document.createElement('option');
|
||||
option.value = status.id;
|
||||
option.textContent = status.name;
|
||||
workStatusSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// 에러 유형 드롭다운
|
||||
const errorTypeSelect = document.getElementById('newErrorTypeSelect');
|
||||
errorTypeSelect.innerHTML = '<option value="">에러 유형을 선택하세요</option>';
|
||||
errorTypes.forEach(error => {
|
||||
const option = document.createElement('option');
|
||||
option.value = error.id;
|
||||
option.textContent = error.name;
|
||||
errorTypeSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
function showNewWorkForm() {
|
||||
document.getElementById('newWorkSection').style.display = 'block';
|
||||
document.getElementById('addNewWorkBtn').style.display = 'none';
|
||||
}
|
||||
|
||||
function hideNewWorkForm() {
|
||||
document.getElementById('newWorkSection').style.display = 'none';
|
||||
document.getElementById('addNewWorkBtn').style.display = 'block';
|
||||
resetNewWorkForm();
|
||||
}
|
||||
|
||||
function resetNewWorkForm() {
|
||||
document.getElementById('newProjectSelect').value = '';
|
||||
document.getElementById('newWorkTypeSelect').value = '';
|
||||
document.getElementById('newWorkStatusSelect').value = '';
|
||||
document.getElementById('newErrorTypeSelect').value = '';
|
||||
document.getElementById('newWorkHours').value = '1.00';
|
||||
document.getElementById('newErrorTypeSection').classList.remove('visible');
|
||||
}
|
||||
|
||||
function toggleErrorTypeSection() {
|
||||
const workStatusSelect = document.getElementById('newWorkStatusSelect');
|
||||
const errorSection = document.getElementById('newErrorTypeSection');
|
||||
const errorTypeSelect = document.getElementById('newErrorTypeSelect');
|
||||
|
||||
if (workStatusSelect.value === '2') { // 에러 상태
|
||||
errorSection.classList.add('visible');
|
||||
errorTypeSelect.setAttribute('required', 'true');
|
||||
} else {
|
||||
errorSection.classList.remove('visible');
|
||||
errorTypeSelect.removeAttribute('required');
|
||||
errorTypeSelect.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
async function saveNewWork() {
|
||||
try {
|
||||
const projectId = document.getElementById('newProjectSelect').value;
|
||||
const workTypeId = document.getElementById('newWorkTypeSelect').value;
|
||||
const workStatusId = document.getElementById('newWorkStatusSelect').value;
|
||||
const errorTypeId = document.getElementById('newErrorTypeSelect').value;
|
||||
const workHours = document.getElementById('newWorkHours').value;
|
||||
|
||||
// 유효성 검사
|
||||
if (!projectId || !workTypeId || !workStatusId || !workHours) {
|
||||
showMessage('모든 필수 필드를 입력해주세요.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (workStatusId === '2' && !errorTypeId) {
|
||||
showMessage('에러 상태일 때는 에러 유형을 선택해야 합니다.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
showMessage('작업을 저장하는 중...', 'loading');
|
||||
|
||||
const workData = {
|
||||
report_date: selectedDate,
|
||||
worker_id: currentWorkerId,
|
||||
project_id: parseInt(projectId),
|
||||
work_type_id: parseInt(workTypeId),
|
||||
work_status_id: parseInt(workStatusId),
|
||||
error_type_id: workStatusId === '2' ? parseInt(errorTypeId) : null,
|
||||
work_hours: parseFloat(workHours),
|
||||
created_by: currentUser?.user_id || 1
|
||||
};
|
||||
|
||||
const response = await window.apiCall(`${window.API}/daily-work-reports`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(workData)
|
||||
});
|
||||
|
||||
showMessage('작업이 성공적으로 저장되었습니다.', 'success');
|
||||
|
||||
// 데이터 새로고침
|
||||
await loadExistingWork();
|
||||
updateWorkerSummary();
|
||||
renderExistingWork();
|
||||
hideNewWorkForm();
|
||||
|
||||
} catch (error) {
|
||||
console.error('작업 저장 오류:', error);
|
||||
showMessage(`작업 저장 중 오류가 발생했습니다: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function editWork(workId) {
|
||||
// TODO: 작업 수정 모달 또는 인라인 편집 구현
|
||||
console.log(`작업 ${workId} 수정`);
|
||||
showMessage('작업 수정 기능은 곧 구현될 예정입니다.', 'info');
|
||||
}
|
||||
|
||||
async function deleteWork(workId) {
|
||||
if (!confirm('이 작업을 삭제하시겠습니까?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showMessage('작업을 삭제하는 중...', 'loading');
|
||||
|
||||
await window.apiCall(`${window.API}/daily-work-reports/${workId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
showMessage('작업이 성공적으로 삭제되었습니다.', 'success');
|
||||
|
||||
// 데이터 새로고침
|
||||
await loadExistingWork();
|
||||
updateWorkerSummary();
|
||||
renderExistingWork();
|
||||
|
||||
} catch (error) {
|
||||
console.error('작업 삭제 오류:', error);
|
||||
showMessage(`작업 삭제 중 오류가 발생했습니다: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleVacationProcess(vacationType) {
|
||||
const vacationNames = {
|
||||
'full': '연차',
|
||||
'half-half': '반반차',
|
||||
'half': '반차'
|
||||
};
|
||||
|
||||
const vacationHours = {
|
||||
'full': 8,
|
||||
'half-half': 6,
|
||||
'half': 4
|
||||
};
|
||||
|
||||
if (!confirm(`${vacationNames[vacationType]} 처리하시겠습니까?\n(${vacationHours[vacationType]}시간으로 자동 입력됩니다)`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showMessage(`${vacationNames[vacationType]} 처리 중...`, 'loading');
|
||||
|
||||
// 휴가용 작업 보고서 생성
|
||||
const vacationWork = {
|
||||
report_date: selectedDate,
|
||||
worker_id: currentWorkerId,
|
||||
project_id: 1, // 기본 프로젝트 (휴가용)
|
||||
work_type_id: 999, // 휴가 전용 작업 유형 (DB에 추가 필요)
|
||||
work_status_id: 1, // 정상 상태
|
||||
error_type_id: null,
|
||||
work_hours: vacationHours[vacationType],
|
||||
created_by: currentUser?.user_id || 1
|
||||
};
|
||||
|
||||
const response = await window.apiCall(`${window.API}/daily-work-reports`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(vacationWork)
|
||||
});
|
||||
|
||||
showMessage(`${vacationNames[vacationType]} 처리가 완료되었습니다.`, 'success');
|
||||
|
||||
// 데이터 새로고침
|
||||
await loadExistingWork();
|
||||
updateWorkerSummary();
|
||||
renderExistingWork();
|
||||
|
||||
} catch (error) {
|
||||
console.error('휴가 처리 오류:', error);
|
||||
showMessage(`휴가 처리 중 오류가 발생했습니다: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 함수로 등록
|
||||
window.editWork = editWork;
|
||||
window.deleteWork = deleteWork;
|
||||
@@ -3,98 +3,284 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>일일 작업보고서 조회</title>
|
||||
<link rel="stylesheet" href="/css/daily-report-viewer.css">
|
||||
<title>작업 현황 확인 - TK 건설</title>
|
||||
<link rel="stylesheet" href="/css/common.css?v=13">
|
||||
<link rel="stylesheet" href="/css/modern-dashboard.css?v=13">
|
||||
<link rel="stylesheet" href="/css/work-report-calendar.css?v=22">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header class="page-header">
|
||||
<h1>📊 일일 작업보고서 조회</h1>
|
||||
<p class="subtitle">날짜를 선택하여 해당일의 작업 현황을 확인하세요</p>
|
||||
</header>
|
||||
|
||||
<div class="date-selector">
|
||||
<div class="date-input-group">
|
||||
<label for="reportDate">📅 조회 날짜:</label>
|
||||
<input type="date" id="reportDate" class="date-input">
|
||||
<button id="searchBtn" class="search-btn">조회</button>
|
||||
<button id="todayBtn" class="today-btn">오늘</button>
|
||||
<!-- 대시보드 헤더 -->
|
||||
<header class="dashboard-header">
|
||||
<div class="header-content">
|
||||
<div class="header-left">
|
||||
<div class="brand">
|
||||
<img src="/img/logo.png" alt="테크니컬코리아" class="brand-logo">
|
||||
<div class="brand-text">
|
||||
<h1 class="brand-title">테크니컬코리아</h1>
|
||||
<p class="brand-subtitle">작업 현황 확인</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-center">
|
||||
<div class="current-time" id="currentTime">
|
||||
<span class="time-label">현재 시각</span>
|
||||
<span class="time-value" id="timeValue">--:--:--</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<div class="user-profile" id="userProfile">
|
||||
<div class="user-avatar">
|
||||
<span class="avatar-text" id="userInitial">사</span>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<span class="user-name" id="userName">사용자</span>
|
||||
<span class="user-role" id="userRole">작업자</span>
|
||||
</div>
|
||||
<div class="profile-menu" id="profileMenu">
|
||||
<a href="/pages/profile/my-profile.html" class="menu-item">
|
||||
<span class="menu-icon">👤</span>
|
||||
내 프로필
|
||||
</a>
|
||||
<a href="/pages/profile/change-password.html" class="menu-item">
|
||||
<span class="menu-icon">🔐</span>
|
||||
비밀번호 변경
|
||||
</a>
|
||||
<a href="/pages/dashboard/group-leader.html" class="menu-item">
|
||||
<span class="menu-icon">📊</span>
|
||||
대시보드
|
||||
</a>
|
||||
<button class="menu-item logout-btn" id="logoutBtn">
|
||||
<span class="menu-icon">🚪</span>
|
||||
로그아웃
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="loadingSpinner" class="loading-spinner" style="display: none;">
|
||||
<!-- 메인 콘텐츠 -->
|
||||
<main class="dashboard-main">
|
||||
<div class="calendar-page-container">
|
||||
<!-- 페이지 제목 -->
|
||||
<div class="page-title-section">
|
||||
<h2 class="page-title">📅 작업 현황 확인</h2>
|
||||
<p class="page-subtitle">월별 작업자 현황을 한눈에 확인하세요</p>
|
||||
</div>
|
||||
|
||||
<!-- 캘린더 카드 -->
|
||||
<div class="calendar-card">
|
||||
<!-- 월 네비게이션 -->
|
||||
<div class="calendar-nav">
|
||||
<button id="prevMonthBtn" class="nav-btn prev-btn">
|
||||
<span class="nav-icon">‹</span>
|
||||
<span class="nav-text">이전</span>
|
||||
</button>
|
||||
|
||||
<div class="calendar-title">
|
||||
<h3 id="monthYearTitle">2025년 11월</h3>
|
||||
<button id="todayBtn" class="today-btn">오늘</button>
|
||||
</div>
|
||||
|
||||
<button id="nextMonthBtn" class="nav-btn next-btn">
|
||||
<span class="nav-text">다음</span>
|
||||
<span class="nav-icon">›</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 범례 -->
|
||||
<div class="calendar-legend">
|
||||
<div class="legend-item">
|
||||
<div class="legend-dot has-overtime-warning"></div>
|
||||
<span>확인필요</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-dot has-errors"></div>
|
||||
<span>미입력</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-dot has-issues"></div>
|
||||
<span>부분입력</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-dot has-normal"></div>
|
||||
<span>이상 없음</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 캘린더 -->
|
||||
<div class="calendar-grid">
|
||||
<div class="calendar-header">
|
||||
<div class="day-header sunday">일</div>
|
||||
<div class="day-header">월</div>
|
||||
<div class="day-header">화</div>
|
||||
<div class="day-header">수</div>
|
||||
<div class="day-header">목</div>
|
||||
<div class="day-header">금</div>
|
||||
<div class="day-header saturday">토</div>
|
||||
</div>
|
||||
<div class="calendar-days" id="calendarDays">
|
||||
<!-- 캘린더 날짜들이 여기에 동적으로 생성됩니다 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 로딩 스피너 -->
|
||||
<div id="loadingSpinner" class="loading-overlay" style="display: none;">
|
||||
<div class="loading-content">
|
||||
<div class="spinner"></div>
|
||||
<p>데이터를 불러오는 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="errorMessage" class="error-message" style="display: none;">
|
||||
<div class="error-content">
|
||||
<span class="error-icon">⚠️</span>
|
||||
<span class="error-text"></span>
|
||||
<!-- 일일 작업 현황 모달 -->
|
||||
<div id="dailyWorkModal" class="modal-overlay" style="display: none;">
|
||||
<div class="modal-container large-modal">
|
||||
<div class="modal-header">
|
||||
<h2 id="modalTitle">2025년 11월 3일 작업 현황</h2>
|
||||
<button class="modal-close-btn" onclick="closeDailyWorkModal()">×</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="noDataMessage" class="no-data-message" style="display: none;">
|
||||
<div class="no-data-content">
|
||||
<span class="no-data-icon">📭</span>
|
||||
<h3>해당 날짜의 작업보고서가 없습니다</h3>
|
||||
<p>다른 날짜를 선택해 주세요.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="reportSummary" class="report-summary" style="display: none;">
|
||||
<div class="summary-cards">
|
||||
<div class="summary-card">
|
||||
<div class="card-header">
|
||||
<span class="card-icon">👥</span>
|
||||
<span class="card-title">작업자 수</span>
|
||||
|
||||
<div class="modal-body">
|
||||
<!-- 요약 정보 -->
|
||||
<div class="daily-summary">
|
||||
<div class="summary-card">
|
||||
<div class="summary-icon success">👥</div>
|
||||
<div class="summary-content">
|
||||
<div class="summary-label">총 작업자</div>
|
||||
<div class="summary-value" id="modalTotalWorkers">0명</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-value" id="totalWorkers">0</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="card-header">
|
||||
<span class="card-icon">⏰</span>
|
||||
<span class="card-title">총 작업시간</span>
|
||||
<div class="summary-card">
|
||||
<div class="summary-icon primary">⏰</div>
|
||||
<div class="summary-content">
|
||||
<div class="summary-label">총 작업시간</div>
|
||||
<div class="summary-value" id="modalTotalHours">0.0h</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-value" id="totalHours">0시간</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="card-header">
|
||||
<span class="card-icon">📝</span>
|
||||
<span class="card-title">작업 항목</span>
|
||||
<div class="summary-card">
|
||||
<div class="summary-icon warning">📝</div>
|
||||
<div class="summary-content">
|
||||
<div class="summary-label">작업 건수</div>
|
||||
<div class="summary-value" id="modalTotalTasks">0건</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-value" id="totalEntries">0개</div>
|
||||
</div>
|
||||
<div class="summary-card error-card">
|
||||
<div class="card-header">
|
||||
<span class="card-icon">⚠️</span>
|
||||
<span class="card-title">에러 항목</span>
|
||||
<div class="summary-card">
|
||||
<div class="summary-icon error">⚠️</div>
|
||||
<div class="summary-content">
|
||||
<div class="summary-label">오류 건수</div>
|
||||
<div class="summary-value" id="modalErrorCount">0건</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-value" id="errorCount">0개</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="workersReport" class="workers-report" style="display: none;">
|
||||
<h2 class="section-title">👥 작업자별 상세 현황</h2>
|
||||
<div id="workersList" class="workers-list">
|
||||
<!-- 작업자별 데이터가 여기에 표시됩니다 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="exportSection" class="export-section" style="display: none;">
|
||||
<h3>📤 데이터 내보내기</h3>
|
||||
<div class="export-buttons">
|
||||
<button id="exportExcelBtn" class="export-btn excel-btn">
|
||||
📊 Excel로 내보내기
|
||||
</button>
|
||||
<button id="printBtn" class="export-btn print-btn">
|
||||
🖨️ 인쇄
|
||||
</button>
|
||||
<!-- 작업자 현황 리스트 -->
|
||||
<div class="modal-work-status">
|
||||
<div class="work-status-header">
|
||||
<h3>작업자별 현황</h3>
|
||||
<div class="status-filter">
|
||||
<select id="statusFilter">
|
||||
<option value="all">전체</option>
|
||||
<option value="incomplete">미입력</option>
|
||||
<option value="partial">부분입력</option>
|
||||
<option value="complete">완료</option>
|
||||
<option value="overtime">연장근로</option>
|
||||
<option value="error">오류</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="modalWorkersList" class="worker-status-list">
|
||||
<!-- 작업자 리스트가 여기에 동적으로 생성됩니다 -->
|
||||
</div>
|
||||
|
||||
<div id="modalNoData" class="empty-state" style="display: none;">
|
||||
<div class="empty-icon">📭</div>
|
||||
<h3>해당 날짜의 작업 보고서가 없습니다</h3>
|
||||
<p>다른 날짜를 선택해 주세요.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/daily-report-viewer.js"></script>
|
||||
<!-- 작업 입력 모달 -->
|
||||
<div id="workEntryModal" class="modal-overlay" style="display: none;">
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<h2 id="workEntryModalTitle">작업 입력</h2>
|
||||
<button class="modal-close-btn" onclick="closeWorkEntryModal()">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<form id="workEntryForm">
|
||||
<!-- 작업자 정보 -->
|
||||
<div class="form-section">
|
||||
<h3>작업자 정보</h3>
|
||||
<div class="form-group">
|
||||
<label class="form-label">작업자</label>
|
||||
<input type="text" id="workerNameDisplay" class="form-control" readonly>
|
||||
<input type="hidden" id="workerId">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">작업 날짜</label>
|
||||
<input type="date" id="workDate" class="form-control" readonly>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 작업 내용 -->
|
||||
<div class="form-section">
|
||||
<h3>작업 내용</h3>
|
||||
<div class="form-group">
|
||||
<label class="form-label">프로젝트 *</label>
|
||||
<select id="projectSelect" class="form-control" required>
|
||||
<option value="">프로젝트를 선택하세요</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">작업 시간 (시간) *</label>
|
||||
<input type="number" id="workHours" class="form-control" min="0" max="24" step="0.5" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">작업 상태 *</label>
|
||||
<select id="workStatusSelect" class="form-control" required>
|
||||
<option value="">상태를 선택하세요</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">작업 설명</label>
|
||||
<textarea id="workDescription" class="form-control" rows="3" placeholder="작업 내용을 상세히 입력하세요"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 휴가 처리 -->
|
||||
<div class="form-section">
|
||||
<h3>휴가 처리</h3>
|
||||
<div class="vacation-buttons">
|
||||
<button type="button" class="btn-vacation" onclick="handleVacation('full')">연차 (8시간)</button>
|
||||
<button type="button" class="btn-vacation" onclick="handleVacation('half')">반차 (4시간)</button>
|
||||
<button type="button" class="btn-vacation" onclick="handleVacation('quarter')">반반차 (2시간)</button>
|
||||
<button type="button" class="btn-vacation" onclick="handleVacation('early')">조퇴 (6시간)</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeWorkEntryModal()">취소</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveWorkEntry()">저장</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript -->
|
||||
<script src="/js/api-config.js?v=13"></script>
|
||||
<script src="/js/auth-check.js?v=13"></script>
|
||||
<script src="/js/load-navbar.js?v=13"></script>
|
||||
<script src="/js/work-report-calendar.js?v=27"></script>
|
||||
</body>
|
||||
</html>
|
||||
170
web-ui/pages/common/worker-individual-report.html
Normal file
170
web-ui/pages/common/worker-individual-report.html
Normal file
@@ -0,0 +1,170 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>개별 작업 보고서 | 테크니컬코리아</title>
|
||||
|
||||
<link rel="stylesheet" href="/css/design-system.css">
|
||||
<link rel="stylesheet" href="/css/daily-work-report.css">
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
|
||||
<script src="/js/api-config.js"></script>
|
||||
<script src="/js/auth-check.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="work-report-container">
|
||||
<!-- 네비게이션 바 -->
|
||||
<div id="navbar-container"></div>
|
||||
|
||||
<!-- 헤더 -->
|
||||
<header class="work-report-header">
|
||||
<h1 id="pageTitle">👤 개별 작업 보고서</h1>
|
||||
<p class="subtitle" id="pageSubtitle">작업자의 일일 작업 내용을 입력하고 수정합니다.</p>
|
||||
</header>
|
||||
|
||||
<!-- 메인 콘텐츠 -->
|
||||
<main class="work-report-main">
|
||||
<!-- 뒤로가기 버튼 -->
|
||||
<a href="javascript:history.back()" class="back-button">
|
||||
← 뒤로가기
|
||||
</a>
|
||||
|
||||
<!-- 작업자 정보 카드 -->
|
||||
<div class="worker-info-card" id="workerInfoCard">
|
||||
<div class="worker-avatar-large">
|
||||
<span id="workerInitial">작</span>
|
||||
</div>
|
||||
<div class="worker-info-details">
|
||||
<h2 id="workerName">작업자명</h2>
|
||||
<p id="workerJob">직종</p>
|
||||
<p id="selectedDate">날짜</p>
|
||||
</div>
|
||||
<div class="worker-status-summary" id="workerStatusSummary">
|
||||
<div class="status-item">
|
||||
<span class="status-label">총 작업시간</span>
|
||||
<span class="status-value" id="totalHours">0h</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">작업 건수</span>
|
||||
<span class="status-value" id="workCount">0건</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 메시지 영역 -->
|
||||
<div id="message-container"></div>
|
||||
|
||||
<!-- 기존 작업 목록 -->
|
||||
<div class="existing-work-section" id="existingWorkSection">
|
||||
<div class="section-header">
|
||||
<h3>📋 기존 작업 목록</h3>
|
||||
<button class="btn btn-primary" id="addNewWorkBtn">
|
||||
➕ 새 작업 추가
|
||||
</button>
|
||||
</div>
|
||||
<div id="existingWorkList">
|
||||
<!-- 기존 작업들이 여기에 표시됩니다 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 새 작업 추가 폼 -->
|
||||
<div class="new-work-section" id="newWorkSection" style="display: none;">
|
||||
<div class="section-header">
|
||||
<h3>➕ 새 작업 추가</h3>
|
||||
<button class="btn btn-secondary" id="cancelNewWorkBtn">
|
||||
✖️ 취소
|
||||
</button>
|
||||
</div>
|
||||
<div class="work-entry" id="newWorkEntry">
|
||||
<div class="work-entry-grid">
|
||||
<div class="form-field-group">
|
||||
<label class="form-field-label">
|
||||
<span class="form-field-icon">🏗️</span>
|
||||
프로젝트
|
||||
</label>
|
||||
<select id="newProjectSelect" class="form-select" required>
|
||||
<option value="">프로젝트를 선택하세요</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-field-group">
|
||||
<label class="form-field-label">
|
||||
<span class="form-field-icon">⚙️</span>
|
||||
작업 유형
|
||||
</label>
|
||||
<select id="newWorkTypeSelect" class="form-select" required>
|
||||
<option value="">작업 유형을 선택하세요</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-field-group">
|
||||
<label class="form-field-label">
|
||||
<span class="form-field-icon">📊</span>
|
||||
업무 상태
|
||||
</label>
|
||||
<select id="newWorkStatusSelect" class="form-select" required>
|
||||
<option value="">업무 상태를 선택하세요</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="error-type-section" id="newErrorTypeSection">
|
||||
<label class="form-field-label">
|
||||
<span class="form-field-icon">⚠️</span>
|
||||
에러 유형
|
||||
</label>
|
||||
<select id="newErrorTypeSelect" class="form-select">
|
||||
<option value="">에러 유형을 선택하세요</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="time-input-section">
|
||||
<label class="form-field-label">
|
||||
<span class="form-field-icon">⏰</span>
|
||||
작업 시간 (시간)
|
||||
</label>
|
||||
<input type="number" id="newWorkHours" class="time-input" step="0.25" min="0.25" max="24" value="1.00" required>
|
||||
<div class="quick-time-buttons">
|
||||
<button type="button" class="quick-time-btn" data-hours="0.5">0.5h</button>
|
||||
<button type="button" class="quick-time-btn" data-hours="1">1h</button>
|
||||
<button type="button" class="quick-time-btn" data-hours="2">2h</button>
|
||||
<button type="button" class="quick-time-btn" data-hours="4">4h</button>
|
||||
<button type="button" class="quick-time-btn" data-hours="8">8h</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-success" id="saveNewWorkBtn">
|
||||
💾 작업 저장
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 휴가 처리 섹션 -->
|
||||
<div class="vacation-section" id="vacationSection">
|
||||
<div class="section-header">
|
||||
<h3>🏖️ 휴가 처리</h3>
|
||||
</div>
|
||||
<div class="vacation-buttons">
|
||||
<button class="btn btn-warning vacation-process-btn" data-type="full">
|
||||
🏖️ 연차 (8시간)
|
||||
</button>
|
||||
<button class="btn btn-warning vacation-process-btn" data-type="half-half">
|
||||
🌤️ 반반차 (6시간)
|
||||
</button>
|
||||
<button class="btn btn-warning vacation-process-btn" data-type="half">
|
||||
🌅 반차 (4시간)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 스크립트 -->
|
||||
<script src="/js/load-navbar.js"></script>
|
||||
<script src="/js/worker-individual-report.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user