feat(sprint004-b): 작업자 월간 확인 페이지 신규 (모바일 전용)
- my-monthly-confirm.html/js/css: 출근부 형식 1인용 확인 페이지 - monthly-comparison.js: 비관리자 → my-monthly-confirm으로 리다이렉트 - 마이그레이션: pages 테이블에 attendance.my_monthly_confirm 등록 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,3 +10,6 @@ WHERE svb.balance_type = 'COMPANY_GRANT'
|
||||
-- 조퇴 휴가 유형 추가 (0.75일 = 반차+반반차)
|
||||
INSERT IGNORE INTO vacation_types (type_code, type_name, deduct_days, is_active, priority)
|
||||
VALUES ('EARLY_LEAVE', '조퇴', 0.75, 1, 10);
|
||||
-- 작업자 월간 확인 페이지 등록
|
||||
INSERT IGNORE INTO pages (page_key, page_name, page_path, category, display_order)
|
||||
VALUES ('attendance.my_monthly_confirm', '월간 근무 확인', '/pages/attendance/my-monthly-confirm.html', '근태 관리', 25);
|
||||
|
||||
150
system1-factory/web/css/my-monthly-confirm.css
Normal file
150
system1-factory/web/css/my-monthly-confirm.css
Normal file
@@ -0,0 +1,150 @@
|
||||
/* my-monthly-confirm.css — 작업자 월간 확인 (모바일 전용) */
|
||||
body { max-width: 480px; margin: 0 auto; }
|
||||
|
||||
/* 월 네비게이션 */
|
||||
.mmc-month-nav {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
gap: 12px; padding: 12px 0; position: relative;
|
||||
}
|
||||
.mmc-month-nav button {
|
||||
width: 36px; height: 36px; border-radius: 50%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
color: #6b7280; background: #f3f4f6; border: none; cursor: pointer;
|
||||
}
|
||||
.mmc-month-nav button:hover { background: #e5e7eb; }
|
||||
.mmc-month-nav span { font-size: 1rem; font-weight: 700; color: #1f2937; }
|
||||
.mmc-status-badge {
|
||||
position: absolute; right: 0; top: 50%; transform: translateY(-50%);
|
||||
font-size: 0.7rem; font-weight: 600; padding: 3px 8px; border-radius: 12px;
|
||||
}
|
||||
.mmc-status-badge.pending { background: #fef3c7; color: #92400e; }
|
||||
.mmc-status-badge.confirmed { background: #dcfce7; color: #166534; }
|
||||
.mmc-status-badge.rejected { background: #fef2f2; color: #991b1b; }
|
||||
|
||||
/* 사용자 정보 */
|
||||
.mmc-user-info {
|
||||
display: flex; align-items: baseline; gap: 8px;
|
||||
padding: 0 4px 8px; font-size: 0.95rem; font-weight: 700; color: #1f2937;
|
||||
}
|
||||
.mmc-user-dept { font-size: 0.8rem; font-weight: 400; color: #6b7280; }
|
||||
|
||||
/* 출근부 테이블 */
|
||||
.mmc-table-wrap { margin-bottom: 12px; }
|
||||
.mmc-table {
|
||||
width: 100%; border-collapse: collapse; background: white;
|
||||
border-radius: 10px; overflow: hidden;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
|
||||
}
|
||||
.mmc-table td { padding: 10px 8px; border-bottom: 1px solid #f3f4f6; }
|
||||
.col-day { width: 36px; text-align: center; font-weight: 600; font-size: 0.85rem; color: #374151; }
|
||||
.col-dow { width: 28px; text-align: center; font-size: 0.8rem; color: #6b7280; }
|
||||
.col-val { text-align: right; font-size: 0.85rem; color: #1f2937; padding-right: 12px; }
|
||||
.dow-sun { color: #ef4444; font-weight: 600; }
|
||||
.dow-sat { color: #3b82f6; font-weight: 600; }
|
||||
|
||||
/* 행 스타일 */
|
||||
.row-holiday td { background: #f9fafb; }
|
||||
.row-holiday-work td { background: #fefce8; }
|
||||
.row-total td { background: #f0fdf4; font-weight: 700; font-size: 0.9rem; border-top: 2px solid #e5e7eb; }
|
||||
|
||||
/* 값 스타일 */
|
||||
.val-vacation { color: #059669; font-weight: 600; }
|
||||
.val-holiday { color: #9ca3af; }
|
||||
|
||||
/* 연차 현황 */
|
||||
.mmc-vac-title { font-size: 0.8rem; font-weight: 600; color: #6b7280; margin-bottom: 6px; padding: 0 4px; }
|
||||
.mmc-vac-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 16px; }
|
||||
.mmc-vac-card {
|
||||
background: white; border-radius: 10px; padding: 12px 8px;
|
||||
text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,0.06);
|
||||
}
|
||||
.mmc-vac-num { font-size: 1.25rem; font-weight: 800; color: #1f2937; }
|
||||
.mmc-vac-num.used { color: #f59e0b; }
|
||||
.mmc-vac-num.remain { color: #059669; }
|
||||
.mmc-vac-label { font-size: 0.7rem; color: #6b7280; margin-top: 2px; }
|
||||
|
||||
/* 확인 상태 */
|
||||
.mmc-confirmed-status {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
justify-content: center; padding: 16px;
|
||||
font-size: 0.85rem; color: #059669; font-weight: 600;
|
||||
margin-bottom: 80px;
|
||||
}
|
||||
|
||||
/* 하단 버튼 */
|
||||
.mmc-bottom-actions {
|
||||
position: fixed; bottom: 0; left: 0; right: 0;
|
||||
display: flex; gap: 8px;
|
||||
padding: 10px 16px calc(10px + env(safe-area-inset-bottom, 0px));
|
||||
background: white; border-top: 1px solid #e5e7eb; z-index: 30;
|
||||
max-width: 480px; margin: 0 auto;
|
||||
}
|
||||
.mmc-confirm-btn {
|
||||
flex: 1; padding: 14px;
|
||||
background: #10b981; color: white;
|
||||
font-size: 0.9rem; font-weight: 700;
|
||||
border: none; border-radius: 12px; cursor: pointer;
|
||||
}
|
||||
.mmc-confirm-btn:hover { background: #059669; }
|
||||
.mmc-reject-btn {
|
||||
flex: 1; padding: 14px;
|
||||
background: white; color: #ef4444;
|
||||
font-size: 0.9rem; font-weight: 700;
|
||||
border: 2px solid #fecaca; border-radius: 12px; cursor: pointer;
|
||||
}
|
||||
.mmc-reject-btn:hover { background: #fef2f2; }
|
||||
|
||||
/* 모달 */
|
||||
.mmc-modal-overlay {
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(0,0,0,0.4); z-index: 50;
|
||||
display: flex; align-items: center; justify-content: center; padding: 16px;
|
||||
}
|
||||
.mmc-modal {
|
||||
background: white; border-radius: 12px;
|
||||
width: 100%; max-width: 400px; overflow: hidden;
|
||||
}
|
||||
.mmc-modal-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 14px 16px; border-bottom: 1px solid #f3f4f6;
|
||||
font-weight: 700; font-size: 0.9rem;
|
||||
}
|
||||
.mmc-modal-header button { background: none; border: none; color: #9ca3af; cursor: pointer; font-size: 1.1rem; }
|
||||
.mmc-modal-body { padding: 16px; }
|
||||
.mmc-modal-desc { font-size: 0.85rem; color: #374151; margin-bottom: 8px; }
|
||||
.mmc-textarea {
|
||||
width: 100%; border: 1px solid #e5e7eb; border-radius: 8px;
|
||||
padding: 10px; font-size: 0.85rem; resize: none;
|
||||
}
|
||||
.mmc-modal-note { font-size: 0.75rem; color: #6b7280; margin-top: 8px; }
|
||||
.mmc-modal-footer {
|
||||
display: flex; gap: 8px; padding: 12px 16px; border-top: 1px solid #f3f4f6;
|
||||
}
|
||||
.mmc-modal-cancel {
|
||||
flex: 1; padding: 10px; border: 1px solid #e5e7eb;
|
||||
border-radius: 8px; background: white; cursor: pointer; font-size: 0.8rem;
|
||||
}
|
||||
.mmc-modal-submit {
|
||||
flex: 1; padding: 10px; background: #ef4444; color: white;
|
||||
border: none; border-radius: 8px; font-size: 0.8rem; font-weight: 600; cursor: pointer;
|
||||
}
|
||||
|
||||
/* 빈 상태 */
|
||||
.mmc-empty {
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
gap: 8px; padding: 48px 16px; color: #9ca3af; font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* 스켈레톤 */
|
||||
.mmc-skeleton {
|
||||
height: 40px;
|
||||
background: linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: mmc-shimmer 1.5s infinite;
|
||||
border-radius: 8px; margin-bottom: 4px;
|
||||
}
|
||||
@keyframes mmc-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
|
||||
|
||||
/* 목록 아래 여백 (하단 고정 버튼 겹침 방지) */
|
||||
.mmc-table-wrap { padding-bottom: 8px; }
|
||||
.mmc-vac-grid { margin-bottom: 80px; }
|
||||
@@ -94,20 +94,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const user = window.currentUser;
|
||||
if (!user) return;
|
||||
|
||||
// mode 결정: URL > role 기반 자동
|
||||
if (urlMode === 'admin') {
|
||||
if (ADMIN_ROLES.includes(user.role)) {
|
||||
currentMode = 'admin';
|
||||
} else {
|
||||
currentMode = 'my';
|
||||
showToast('관리자 전용 기능입니다', 'error');
|
||||
}
|
||||
} else if (currentUserId && ADMIN_ROLES.includes(user.role)) {
|
||||
// 비관리자 → 작업자 전용 확인 페이지로 리다이렉트
|
||||
if (!ADMIN_ROLES.includes(user.role)) {
|
||||
location.href = '/pages/attendance/my-monthly-confirm.html';
|
||||
return;
|
||||
}
|
||||
|
||||
// 관리자 mode 결정
|
||||
if (currentUserId) {
|
||||
currentMode = 'detail';
|
||||
} else if (!urlMode && ADMIN_ROLES.includes(user.role)) {
|
||||
currentMode = 'admin';
|
||||
} else {
|
||||
currentMode = 'my';
|
||||
currentMode = 'admin';
|
||||
}
|
||||
|
||||
// 관리자 뷰 전환 버튼 (관리자만)
|
||||
|
||||
271
system1-factory/web/js/my-monthly-confirm.js
Normal file
271
system1-factory/web/js/my-monthly-confirm.js
Normal file
@@ -0,0 +1,271 @@
|
||||
/**
|
||||
* my-monthly-confirm.js — 작업자 월간 근무 확인 (모바일 전용)
|
||||
* Sprint 004 Section B
|
||||
*/
|
||||
|
||||
const DAYS_KR = ['일', '월', '화', '수', '목', '금', '토'];
|
||||
const VAC_TEXT = {
|
||||
'ANNUAL_FULL': '연차', 'ANNUAL_HALF': '반차', 'ANNUAL_QUARTER': '반반차',
|
||||
'EARLY_LEAVE': '조퇴', 'SICK': '병가', 'SICK_FULL': '병가', 'SICK_HALF': '병가(반)',
|
||||
'SPECIAL': '경조사', 'SPOUSE_BIRTH': '출산휴가'
|
||||
};
|
||||
|
||||
let currentYear, currentMonth;
|
||||
let isProcessing = false;
|
||||
|
||||
// ===== Init =====
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var now = new Date();
|
||||
currentYear = now.getFullYear();
|
||||
currentMonth = now.getMonth() + 1;
|
||||
|
||||
var params = new URLSearchParams(location.search);
|
||||
if (params.get('year')) currentYear = parseInt(params.get('year'));
|
||||
if (params.get('month')) currentMonth = parseInt(params.get('month'));
|
||||
|
||||
setTimeout(function() {
|
||||
if (!window.currentUser) return;
|
||||
updateMonthLabel();
|
||||
loadData();
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// ===== Month Nav =====
|
||||
function updateMonthLabel() {
|
||||
document.getElementById('monthLabel').textContent = currentYear + '년 ' + currentMonth + '월';
|
||||
}
|
||||
|
||||
function changeMonth(delta) {
|
||||
currentMonth += delta;
|
||||
if (currentMonth > 12) { currentMonth = 1; currentYear++; }
|
||||
if (currentMonth < 1) { currentMonth = 12; currentYear--; }
|
||||
updateMonthLabel();
|
||||
loadData();
|
||||
}
|
||||
|
||||
// ===== Data Load =====
|
||||
async function loadData() {
|
||||
var tableWrap = document.getElementById('tableWrap');
|
||||
tableWrap.innerHTML = '<div class="mmc-skeleton"></div><div class="mmc-skeleton"></div><div class="mmc-skeleton"></div>';
|
||||
|
||||
try {
|
||||
var userId = window.currentUser.user_id || window.currentUser.id;
|
||||
var [recordsRes, balanceRes] = await Promise.all([
|
||||
window.apiCall('/monthly-comparison/my-records?year=' + currentYear + '&month=' + currentMonth),
|
||||
window.apiCall('/attendance/vacation-balance/' + userId).catch(function() { return { success: true, data: [] }; })
|
||||
]);
|
||||
|
||||
if (!recordsRes || !recordsRes.success) {
|
||||
tableWrap.innerHTML = '<div class="mmc-empty"><i class="fas fa-calendar-xmark text-2xl text-gray-300"></i><p>데이터가 없습니다</p></div>';
|
||||
document.getElementById('bottomActions').classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
var data = recordsRes.data;
|
||||
renderUserInfo(data.user);
|
||||
renderAttendanceTable(data.daily_records || []);
|
||||
renderVacationBalance(balanceRes.data || []);
|
||||
renderConfirmStatus(data.confirmation);
|
||||
} catch (e) {
|
||||
tableWrap.innerHTML = '<div class="mmc-empty"><i class="fas fa-exclamation-triangle text-2xl text-red-300"></i><p>네트워크 오류</p></div>';
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Render =====
|
||||
function renderUserInfo(user) {
|
||||
if (!user) return;
|
||||
document.getElementById('userName').textContent = user.worker_name || user.name || '-';
|
||||
document.getElementById('userDept').textContent =
|
||||
(user.job_type ? user.job_type + ' · ' : '') + (user.department_name || '');
|
||||
}
|
||||
|
||||
function renderAttendanceTable(records) {
|
||||
var el = document.getElementById('tableWrap');
|
||||
if (!records.length) {
|
||||
el.innerHTML = '<div class="mmc-empty"><p>해당 월 데이터가 없습니다</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
var totalHours = 0;
|
||||
var html = '<table class="mmc-table"><tbody>';
|
||||
|
||||
records.forEach(function(r) {
|
||||
var day = parseInt(r.date.substring(8));
|
||||
var dow = r.day_of_week || DAYS_KR[new Date(r.date).getDay()];
|
||||
var isHoliday = r.is_holiday;
|
||||
var rowClass = isHoliday ? ' class="row-holiday"' : '';
|
||||
|
||||
// 셀 값 결정
|
||||
var val = '-';
|
||||
var valClass = '';
|
||||
|
||||
if (r.attendance && r.attendance.vacation_type) {
|
||||
val = r.attendance.vacation_type;
|
||||
valClass = 'val-vacation';
|
||||
} else if (isHoliday && (!r.attendance || !r.attendance.total_work_hours)) {
|
||||
val = '휴무';
|
||||
valClass = 'val-holiday';
|
||||
} else if (r.attendance && r.attendance.total_work_hours > 0) {
|
||||
var hrs = parseFloat(r.attendance.total_work_hours);
|
||||
val = hrs.toFixed(2);
|
||||
totalHours += hrs;
|
||||
} else if (r.work_report && r.work_report.total_hours > 0) {
|
||||
var hrs2 = parseFloat(r.work_report.total_hours);
|
||||
val = hrs2.toFixed(2);
|
||||
totalHours += hrs2;
|
||||
}
|
||||
|
||||
// 주말 근무
|
||||
if (isHoliday && r.attendance && r.attendance.total_work_hours > 0) {
|
||||
val = parseFloat(r.attendance.total_work_hours).toFixed(2);
|
||||
totalHours += parseFloat(r.attendance.total_work_hours);
|
||||
rowClass = ' class="row-holiday-work"';
|
||||
valClass = '';
|
||||
}
|
||||
|
||||
var dowClass = (dow === '일') ? 'dow-sun' : (dow === '토') ? 'dow-sat' : '';
|
||||
|
||||
html += '<tr' + rowClass + '>';
|
||||
html += '<td class="col-day">' + day + '</td>';
|
||||
html += '<td class="col-dow ' + dowClass + '">' + dow + '</td>';
|
||||
html += '<td class="col-val ' + valClass + '">' + escHtml(val) + '</td>';
|
||||
html += '</tr>';
|
||||
});
|
||||
|
||||
html += '</tbody>';
|
||||
html += '<tfoot><tr class="row-total">';
|
||||
html += '<td colspan="2">총 근무시간</td>';
|
||||
html += '<td class="col-val"><strong>' + totalHours.toFixed(2) + 'h</strong></td>';
|
||||
html += '</tr></tfoot></table>';
|
||||
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
function renderVacationBalance(balances) {
|
||||
var el = document.getElementById('vacationCards');
|
||||
var total = 0, used = 0;
|
||||
|
||||
if (Array.isArray(balances)) {
|
||||
balances.forEach(function(b) {
|
||||
total += parseFloat(b.total_days || b.remaining_days_total || 0);
|
||||
used += parseFloat(b.used_days || 0);
|
||||
});
|
||||
}
|
||||
|
||||
var remaining = total - used;
|
||||
el.innerHTML =
|
||||
'<div class="mmc-vac-title">연차 현황</div>' +
|
||||
'<div class="mmc-vac-grid">' +
|
||||
'<div class="mmc-vac-card"><div class="mmc-vac-num">' + fmtNum(total) + '</div><div class="mmc-vac-label">신규</div></div>' +
|
||||
'<div class="mmc-vac-card"><div class="mmc-vac-num used">' + fmtNum(used) + '</div><div class="mmc-vac-label">사용</div></div>' +
|
||||
'<div class="mmc-vac-card"><div class="mmc-vac-num remain">' + fmtNum(remaining) + '</div><div class="mmc-vac-label">잔여</div></div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function renderConfirmStatus(conf) {
|
||||
var actions = document.getElementById('bottomActions');
|
||||
var statusEl = document.getElementById('confirmedStatus');
|
||||
var badge = document.getElementById('statusBadge');
|
||||
|
||||
if (!conf || conf.status === 'pending') {
|
||||
actions.classList.remove('hidden');
|
||||
statusEl.classList.add('hidden');
|
||||
badge.textContent = '미확인';
|
||||
badge.className = 'mmc-status-badge pending';
|
||||
return;
|
||||
}
|
||||
|
||||
if (conf.status === 'confirmed') {
|
||||
actions.classList.add('hidden');
|
||||
statusEl.classList.remove('hidden');
|
||||
badge.textContent = '확인완료';
|
||||
badge.className = 'mmc-status-badge confirmed';
|
||||
var dt = conf.confirmed_at ? new Date(conf.confirmed_at).toLocaleDateString('ko') : '';
|
||||
document.getElementById('confirmedText').textContent = dt + ' 확인 완료';
|
||||
} else if (conf.status === 'rejected') {
|
||||
actions.classList.remove('hidden');
|
||||
statusEl.classList.add('hidden');
|
||||
badge.textContent = '반려';
|
||||
badge.className = 'mmc-status-badge rejected';
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Actions =====
|
||||
async function confirmMonth() {
|
||||
if (isProcessing) return;
|
||||
if (!confirm(currentYear + '년 ' + currentMonth + '월 근무 내역을 확인하시겠습니까?')) return;
|
||||
|
||||
isProcessing = true;
|
||||
try {
|
||||
var res = await window.apiCall('/monthly-comparison/confirm', 'POST', {
|
||||
year: currentYear, month: currentMonth, status: 'confirmed'
|
||||
});
|
||||
if (res && res.success) {
|
||||
showToast(res.message || '확인 완료', 'success');
|
||||
loadData();
|
||||
} else {
|
||||
showToast(res && res.message || '처리 실패', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('네트워크 오류', 'error');
|
||||
} finally {
|
||||
isProcessing = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openRejectModal() {
|
||||
document.getElementById('rejectReason').value = '';
|
||||
document.getElementById('rejectModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeRejectModal() {
|
||||
document.getElementById('rejectModal').classList.add('hidden');
|
||||
}
|
||||
|
||||
async function submitReject() {
|
||||
if (isProcessing) return;
|
||||
var reason = document.getElementById('rejectReason').value.trim();
|
||||
if (!reason) { showToast('반려 사유를 입력해주세요', 'error'); return; }
|
||||
|
||||
isProcessing = true;
|
||||
try {
|
||||
var res = await window.apiCall('/monthly-comparison/confirm', 'POST', {
|
||||
year: currentYear, month: currentMonth, status: 'rejected', reject_reason: reason
|
||||
});
|
||||
if (res && res.success) {
|
||||
showToast(res.message || '반려 제출 완료', 'success');
|
||||
closeRejectModal();
|
||||
loadData();
|
||||
} else {
|
||||
showToast(res && res.message || '처리 실패', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('네트워크 오류', 'error');
|
||||
} finally {
|
||||
isProcessing = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Helpers =====
|
||||
function fmtNum(v) {
|
||||
var n = parseFloat(v) || 0;
|
||||
return n % 1 === 0 ? n.toString() : n.toFixed(1);
|
||||
}
|
||||
|
||||
function escHtml(s) {
|
||||
return (s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function showToast(msg, type) {
|
||||
if (window.showToast) { window.showToast(msg, type); return; }
|
||||
var c = document.getElementById('toastContainer');
|
||||
var t = document.createElement('div');
|
||||
t.className = 'toast toast-' + (type || 'info');
|
||||
t.textContent = msg;
|
||||
c.appendChild(t);
|
||||
setTimeout(function() { t.remove(); }, 3000);
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') closeRejectModal();
|
||||
});
|
||||
@@ -159,7 +159,7 @@
|
||||
|
||||
<script src="/static/js/tkfb-core.js?v=2026033001"></script>
|
||||
<script src="/js/api-base.js?v=2026031701"></script>
|
||||
<script src="/js/monthly-comparison.js?v=2026033102"></script>
|
||||
<script src="/js/monthly-comparison.js?v=2026033103"></script>
|
||||
<script>initAuth();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
109
system1-factory/web/pages/attendance/my-monthly-confirm.html
Normal file
109
system1-factory/web/pages/attendance/my-monthly-confirm.html
Normal file
@@ -0,0 +1,109 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>월간 근무 확인 - TK 공장관리</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>tailwind.config = { corePlugins: { preflight: false } }</script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="/static/css/tkfb.css?v=2026033001">
|
||||
<link rel="stylesheet" href="/css/my-monthly-confirm.css?v=2026033101">
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
<header class="bg-orange-700 text-white sticky top-0 z-50">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center h-14">
|
||||
<div class="flex items-center gap-3">
|
||||
<button id="mobileMenuBtn" class="lg:hidden text-orange-200 hover:text-white"><i class="fas fa-bars text-xl"></i></button>
|
||||
<i class="fas fa-industry text-xl text-orange-200"></i>
|
||||
<h1 class="text-lg font-semibold">TK 공장관리</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span id="headerUserName" class="text-sm hidden sm:block">-</span>
|
||||
<div id="headerUserAvatar" class="w-8 h-8 bg-orange-600 rounded-full flex items-center justify-center text-sm font-bold">-</div>
|
||||
<button onclick="doLogout()" class="text-orange-200 hover:text-white" title="로그아웃"><i class="fas fa-sign-out-alt"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></div>
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 fade-in">
|
||||
<div class="flex gap-6">
|
||||
<nav id="sideNav" class="hidden lg:flex flex-col gap-1 w-52 flex-shrink-0 pt-2 fixed lg:static z-40 bg-white lg:bg-transparent p-4 lg:p-0 rounded-lg lg:rounded-none shadow-lg lg:shadow-none top-14 left-0 bottom-0 overflow-y-auto"></nav>
|
||||
<div class="flex-1 min-w-0">
|
||||
|
||||
<!-- 월 네비게이션 -->
|
||||
<div class="mmc-month-nav">
|
||||
<button type="button" onclick="changeMonth(-1)"><i class="fas fa-chevron-left"></i></button>
|
||||
<span id="monthLabel">2026년 3월</span>
|
||||
<button type="button" onclick="changeMonth(1)"><i class="fas fa-chevron-right"></i></button>
|
||||
<div class="mmc-status-badge" id="statusBadge"></div>
|
||||
</div>
|
||||
|
||||
<!-- 사용자 정보 -->
|
||||
<div class="mmc-user-info" id="userInfo">
|
||||
<span id="userName">-</span>
|
||||
<span id="userDept" class="mmc-user-dept">-</span>
|
||||
</div>
|
||||
|
||||
<!-- 출근부 테이블 -->
|
||||
<div class="mmc-table-wrap" id="tableWrap">
|
||||
<div class="mmc-skeleton"></div>
|
||||
<div class="mmc-skeleton"></div>
|
||||
</div>
|
||||
|
||||
<!-- 연차 현황 -->
|
||||
<div class="mmc-vacation-cards" id="vacationCards"></div>
|
||||
|
||||
<!-- 확인 상태 메시지 -->
|
||||
<div class="mmc-confirmed-status hidden" id="confirmedStatus">
|
||||
<i class="fas fa-check-circle text-green-500"></i>
|
||||
<span id="confirmedText"></span>
|
||||
</div>
|
||||
|
||||
<!-- 확인/반려 버튼 -->
|
||||
<div class="mmc-bottom-actions" id="bottomActions">
|
||||
<button type="button" class="mmc-confirm-btn" id="confirmBtn" onclick="confirmMonth()">
|
||||
<i class="fas fa-check-circle mr-2"></i>확인 완료
|
||||
</button>
|
||||
<button type="button" class="mmc-reject-btn" id="rejectBtn" onclick="openRejectModal()">
|
||||
<i class="fas fa-times-circle mr-2"></i>문제 있음
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 반려 모달 -->
|
||||
<div class="mmc-modal-overlay hidden" id="rejectModal">
|
||||
<div class="mmc-modal">
|
||||
<div class="mmc-modal-header">
|
||||
<span><i class="fas fa-times-circle text-red-500 mr-2"></i>문제 있음 (반려)</span>
|
||||
<button type="button" onclick="closeRejectModal()"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<div class="mmc-modal-body">
|
||||
<p class="mmc-modal-desc">반려 사유를 입력해주세요:</p>
|
||||
<textarea id="rejectReason" class="mmc-textarea" rows="3" placeholder="예: 3/2 근무시간이 실제와 다릅니다"></textarea>
|
||||
<p class="mmc-modal-note">
|
||||
<i class="fas fa-info-circle text-blue-400 mr-1"></i>
|
||||
반려 시 생산지원팀에 알림이 전달됩니다.
|
||||
</p>
|
||||
</div>
|
||||
<div class="mmc-modal-footer">
|
||||
<button type="button" class="mmc-modal-cancel" onclick="closeRejectModal()">취소</button>
|
||||
<button type="button" class="mmc-modal-submit" onclick="submitReject()">반려 제출</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast -->
|
||||
<div id="toastContainer" class="toast-container"></div>
|
||||
|
||||
<script src="/static/js/tkfb-core.js?v=2026033001"></script>
|
||||
<script src="/js/api-base.js?v=2026031701"></script>
|
||||
<script src="/js/my-monthly-confirm.js?v=2026033101"></script>
|
||||
<script>initAuth();</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user