feat(proxy-input): 연차 정보 연동 — 연차 작업자 비활성화 + 뱃지
- 모델: getDailyStatus에 vacation_type 쿼리 추가 - 프론트: 연차(ANNUAL_FULL) 카드 비활성화 + 선택/일괄설정/저장에서 제외 - 반차/반반차/조퇴: 뱃지 표시 + 근무시간 자동 조정 (4h/6h/2h) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -99,6 +99,17 @@ const ProxyInputModel = {
|
||||
GROUP BY dwr.user_id
|
||||
`, [date]);
|
||||
|
||||
// 4. 해당 날짜의 연차 기록
|
||||
const [vacationRecords] = await db.query(`
|
||||
SELECT dar.user_id, dar.vacation_type_id,
|
||||
vt.type_code AS vacation_type_code,
|
||||
vt.type_name AS vacation_type_name,
|
||||
vt.deduct_days
|
||||
FROM daily_attendance_records dar
|
||||
JOIN vacation_types vt ON dar.vacation_type_id = vt.id
|
||||
WHERE dar.record_date = ? AND dar.vacation_type_id IS NOT NULL
|
||||
`, [date]);
|
||||
|
||||
// 메모리에서 조합
|
||||
const tbmMap = {};
|
||||
tbmAssignments.forEach(ta => {
|
||||
@@ -109,6 +120,9 @@ const ProxyInputModel = {
|
||||
const reportMap = {};
|
||||
reports.forEach(r => { reportMap[r.user_id] = r; });
|
||||
|
||||
const vacMap = {};
|
||||
vacationRecords.forEach(v => { vacMap[v.user_id] = v; });
|
||||
|
||||
let tbmCompleted = 0, reportCompleted = 0, bothCompleted = 0, bothMissing = 0;
|
||||
|
||||
const workerList = workers.map(w => {
|
||||
@@ -120,6 +134,7 @@ const ProxyInputModel = {
|
||||
is_proxy_input: !!ta.is_proxy_input
|
||||
}));
|
||||
const totalReportHours = reportMap[w.user_id]?.total_hours || 0;
|
||||
const vac = vacMap[w.user_id] || null;
|
||||
|
||||
let status = 'both_missing';
|
||||
if (hasTbm && hasReport) { status = 'complete'; bothCompleted++; }
|
||||
@@ -133,7 +148,11 @@ const ProxyInputModel = {
|
||||
return {
|
||||
user_id: w.user_id, worker_name: w.worker_name, job_type: w.job_type,
|
||||
department_name: w.department_name, has_tbm: hasTbm, has_report: hasReport,
|
||||
tbm_sessions: tbmSessions, total_report_hours: totalReportHours, status
|
||||
tbm_sessions: tbmSessions, total_report_hours: totalReportHours, status,
|
||||
vacation_type_id: vac ? vac.vacation_type_id : null,
|
||||
vacation_type_code: vac ? vac.vacation_type_code : null,
|
||||
vacation_type_name: vac ? vac.vacation_type_name : null,
|
||||
vacation_hours: vac ? (8 - parseFloat(vac.deduct_days) * 8) : null
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -226,6 +226,17 @@
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
@keyframes ds-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
|
||||
|
||||
/* 연차 비활성화 */
|
||||
.pi-card.vacation-disabled { opacity: 0.5; }
|
||||
.pi-card.vacation-disabled .pi-card-form { pointer-events: none; }
|
||||
.pi-card.vacation-disabled .pi-card-header { cursor: default; }
|
||||
.pi-vac-badge {
|
||||
font-size: 0.65rem; font-weight: 600;
|
||||
padding: 2px 6px; border-radius: 4px;
|
||||
background: #dcfce7; color: #166534;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.ds-empty { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 48px 16px; color: #9ca3af; font-size: 0.875rem; }
|
||||
.ds-link { color: #2563eb; font-size: 0.8rem; text-decoration: underline; }
|
||||
|
||||
|
||||
@@ -146,17 +146,25 @@ async function loadWorkers() {
|
||||
function renderCards() {
|
||||
const cardsEl = document.getElementById('workerCards');
|
||||
cardsEl.innerHTML = missingWorkers.map(w => {
|
||||
const statusLabel = { both_missing: 'TBM+보고서 미입력', tbm_only: '보고서만 미입력', report_only: 'TBM만 미입력' }[w.status] || '';
|
||||
const isFullVacation = w.vacation_type_code === 'ANNUAL_FULL';
|
||||
const hasVacation = !!w.vacation_type_code;
|
||||
const statusLabel = isFullVacation ? ''
|
||||
: ({ both_missing: 'TBM+보고서 미입력', tbm_only: '보고서만 미입력', report_only: 'TBM만 미입력' }[w.status] || '');
|
||||
const fd = workerFormData[w.user_id] || getDefaultFormData(w);
|
||||
if (hasVacation && !isFullVacation && w.vacation_hours != null) {
|
||||
fd.work_hours = w.vacation_hours;
|
||||
}
|
||||
workerFormData[w.user_id] = fd;
|
||||
const sel = selectedIds.has(w.user_id);
|
||||
const vacBadge = hasVacation ? '<span class="pi-vac-badge">' + escHtml(w.vacation_type_name) + '</span>' : '';
|
||||
const disabledClass = isFullVacation ? ' vacation-disabled' : '';
|
||||
|
||||
return `
|
||||
<div class="pi-card ${sel ? 'selected' : ''}" id="card-${w.user_id}">
|
||||
<div class="pi-card ${sel ? 'selected' : ''}${disabledClass}" id="card-${w.user_id}">
|
||||
<div class="pi-card-header" onclick="toggleWorker(${w.user_id})">
|
||||
<div class="pi-card-check">${sel ? '<i class="fas fa-check text-xs"></i>' : ''}</div>
|
||||
<div class="pi-card-check">${isFullVacation ? '' : (sel ? '<i class="fas fa-check text-xs"></i>' : '')}</div>
|
||||
<div>
|
||||
<div class="pi-card-name">${escHtml(w.worker_name)}</div>
|
||||
<div class="pi-card-name">${escHtml(w.worker_name)} ${vacBadge}</div>
|
||||
<div class="pi-card-meta">${escHtml(w.job_type)} · ${escHtml(w.department_name)}</div>
|
||||
</div>
|
||||
<div class="pi-card-status ${w.status}">${statusLabel}</div>
|
||||
@@ -228,6 +236,8 @@ function escHtml(s) { return (s || '').replace(/&/g, '&').replace(/</g, '<
|
||||
|
||||
// ===== Worker Toggle =====
|
||||
function toggleWorker(userId) {
|
||||
var worker = missingWorkers.find(function(w) { return w.user_id === userId; });
|
||||
if (worker && worker.vacation_type_code === 'ANNUAL_FULL') return;
|
||||
if (selectedIds.has(userId)) {
|
||||
selectedIds.delete(userId);
|
||||
} else {
|
||||
@@ -297,8 +307,10 @@ function applyBulk(field, value) {
|
||||
|
||||
if (hasExisting) {
|
||||
if (!confirm('이미 입력된 값이 있습니다. 덮어쓰시겠습니까?')) {
|
||||
// 빈 필드만 채움
|
||||
// 빈 필드만 채움 (연차 작업자 skip)
|
||||
for (const uid of selectedIds) {
|
||||
var bw = missingWorkers.find(function(w) { return w.user_id === uid; });
|
||||
if (bw && bw.vacation_type_code === 'ANNUAL_FULL') continue;
|
||||
if (!workerFormData[uid][field] || workerFormData[uid][field] === '') {
|
||||
workerFormData[uid][field] = value;
|
||||
}
|
||||
@@ -310,6 +322,8 @@ function applyBulk(field, value) {
|
||||
}
|
||||
|
||||
for (const uid of selectedIds) {
|
||||
var bw2 = missingWorkers.find(function(w) { return w.user_id === uid; });
|
||||
if (bw2 && bw2.vacation_type_code === 'ANNUAL_FULL') continue;
|
||||
workerFormData[uid][field] = value;
|
||||
if (field === 'work_type_id') workerFormData[uid].task_id = '';
|
||||
}
|
||||
@@ -337,6 +351,15 @@ function updateSaveBtn() {
|
||||
async function saveProxyInput() {
|
||||
if (saving || selectedIds.size === 0) return;
|
||||
|
||||
// 연차 작업자 선택 해제 (안전장치)
|
||||
for (const uid of selectedIds) {
|
||||
const ww = missingWorkers.find(x => x.user_id === uid);
|
||||
if (ww && ww.vacation_type_code === 'ANNUAL_FULL') {
|
||||
selectedIds.delete(uid);
|
||||
}
|
||||
}
|
||||
if (selectedIds.size === 0) { showToast('저장할 대상이 없습니다', 'error'); return; }
|
||||
|
||||
// 유효성 검사
|
||||
for (const uid of selectedIds) {
|
||||
const fd = workerFormData[uid];
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<title>대리입력 - TK 공장관리</title>
|
||||
<script src="https://cdn.tailwindcss.com"></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/proxy-input.css?v=2026033001">
|
||||
<link rel="stylesheet" href="/static/css/tkfb.css?v=2026033102">
|
||||
<link rel="stylesheet" href="/css/proxy-input.css?v=2026033102">
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
<header class="bg-orange-700 text-white sticky top-0 z-50">
|
||||
@@ -113,9 +113,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/tkfb-core.js?v=2026033001"></script>
|
||||
<script src="/static/js/tkfb-core.js?v=2026033102"></script>
|
||||
<script src="/js/api-base.js?v=2026031701"></script>
|
||||
<script src="/js/proxy-input.js?v=2026033001"></script>
|
||||
<script src="/js/proxy-input.js?v=2026033102"></script>
|
||||
<script>initAuth();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user