feat(vacation): 이월 연차 소진/만료 구분 표시
- isExpired() 함수: 만료일 다음날부터 만료 (today > expires_at) - 이월 셀에 만료 시 "소진 X + 만료 Y" 표시 - 소진: 초록 (#10b981) - 만료: 회색 취소선 (#9ca3af) - loadData()에 carryoverUsed, carryoverExpiresAt 추가 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -232,6 +232,41 @@
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
/* 아코디언 */
|
||||
.month-accordion { margin-bottom: 0.25rem; }
|
||||
.month-header {
|
||||
cursor: pointer; padding: 0.5rem;
|
||||
background: #f9fafb; border-radius: 0.375rem;
|
||||
font-weight: 600; font-size: 0.85rem;
|
||||
display: flex; align-items: center; gap: 0.5rem;
|
||||
user-select: none;
|
||||
}
|
||||
.month-header:hover { background: #f3f4f6; }
|
||||
.month-arrow { font-size: 0.7rem; width: 1rem; text-align: center; }
|
||||
.month-body { padding: 0.25rem 0 0.25rem 1.5rem; }
|
||||
.record-row {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
padding: 0.3rem 0; border-bottom: 1px solid #f3f4f6;
|
||||
font-size: 0.8rem; gap: 0.5rem;
|
||||
}
|
||||
.record-row .record-info { flex: 1; }
|
||||
.record-row .record-days { color: #6b7280; white-space: nowrap; }
|
||||
.record-actions { display: flex; gap: 0.25rem; white-space: nowrap; }
|
||||
.record-actions button {
|
||||
font-size: 0.7rem; padding: 0.15rem 0.4rem;
|
||||
border: 1px solid #d1d5db; border-radius: 0.25rem;
|
||||
background: white; cursor: pointer;
|
||||
}
|
||||
.record-actions button:hover { background: #f3f4f6; }
|
||||
.record-actions .btn-del { color: #dc2626; border-color: #fca5a5; }
|
||||
.record-actions .btn-del:hover { background: #fee2e2; }
|
||||
/* 인라인 편집 */
|
||||
.edit-row { display: flex; align-items: center; gap: 0.5rem; padding: 0.3rem 0; border-bottom: 1px solid #f3f4f6; font-size: 0.8rem; }
|
||||
.edit-row input[type="date"] { padding: 0.2rem 0.3rem; border: 1px solid #93c5fd; border-radius: 0.25rem; font-size: 0.78rem; }
|
||||
.edit-row select { padding: 0.2rem 0.3rem; border: 1px solid #93c5fd; border-radius: 0.25rem; font-size: 0.78rem; }
|
||||
.edit-row .btn-save { background: #3b82f6; color: white; border: none; padding: 0.2rem 0.5rem; border-radius: 0.25rem; font-size: 0.7rem; cursor: pointer; }
|
||||
.edit-row .btn-cancel { background: #f3f4f6; border: 1px solid #d1d5db; padding: 0.2rem 0.5rem; border-radius: 0.25rem; font-size: 0.7rem; cursor: pointer; }
|
||||
|
||||
.loading { text-align: center; padding: 2rem; color: #6b7280; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@@ -371,6 +406,14 @@
|
||||
}, 50);
|
||||
})();
|
||||
|
||||
// 만료 판단: 만료일 다음날부터 만료 (2/28 만료 → 3/1부터)
|
||||
function isExpired(expiresAt) {
|
||||
if (!expiresAt) return false;
|
||||
const today = new Date(); today.setHours(0,0,0,0);
|
||||
const exp = new Date(expiresAt); exp.setHours(0,0,0,0);
|
||||
return today > exp;
|
||||
}
|
||||
|
||||
// 숫자 포맷: 정수면 소수점 없이, 소수면 2자리
|
||||
function fmtNum(v) {
|
||||
const n = parseFloat(v) || 0;
|
||||
@@ -383,6 +426,12 @@
|
||||
let vacationData = {}; // { workerId: { carryover, annual, longService, specials: [{type, days}], totalUsed } }
|
||||
let currentWorkerId = null;
|
||||
|
||||
// 월별 세부 모달 상태
|
||||
let currentModalUserId = null;
|
||||
let currentModalName = '';
|
||||
let editBackup = {};
|
||||
const DAYS_KR = ['일', '월', '화', '수', '목', '금', '토'];
|
||||
|
||||
// 경조사 유형
|
||||
const specialTypes = [
|
||||
{ code: 'WEDDING', name: '결혼', defaultDays: 5 },
|
||||
@@ -446,6 +495,8 @@
|
||||
workers.forEach(w => {
|
||||
vacationData[w.user_id] = {
|
||||
carryover: 0,
|
||||
carryoverUsed: 0,
|
||||
carryoverExpiresAt: null,
|
||||
annual: 0,
|
||||
longService: 0,
|
||||
specials: [],
|
||||
@@ -462,6 +513,8 @@
|
||||
|
||||
if (btype === 'CARRY_OVER' || code === 'CARRYOVER' || b.type_name === '이월') {
|
||||
data.carryover += parseFloat(b.total_days) || 0;
|
||||
data.carryoverUsed += parseFloat(b.used_days) || 0;
|
||||
data.carryoverExpiresAt = b.expires_at || data.carryoverExpiresAt;
|
||||
data.totalUsed += parseFloat(b.used_days) || 0;
|
||||
} else if (btype === 'LONG_SERVICE') {
|
||||
data.longService += parseFloat(b.total_days) || 0;
|
||||
@@ -500,8 +553,11 @@
|
||||
}
|
||||
|
||||
tbody.innerHTML = workers.map((w, idx) => {
|
||||
const d = vacationData[w.user_id] || { carryover: 0, annual: 0, longService: 0, specials: [], totalUsed: 0 };
|
||||
const d = vacationData[w.user_id] || { carryover: 0, carryoverUsed: 0, carryoverExpiresAt: null, annual: 0, longService: 0, specials: [], totalUsed: 0 };
|
||||
const carryover = parseFloat(d.carryover) || 0;
|
||||
const carryoverExpired = isExpired(d.carryoverExpiresAt);
|
||||
const carryoverUsed = parseFloat(d.carryoverUsed) || 0;
|
||||
const carryoverLapsed = carryoverExpired ? Math.max(0, carryover - carryoverUsed) : 0;
|
||||
const annual = parseFloat(d.annual) || 0;
|
||||
const longService = parseFloat(d.longService) || 0;
|
||||
const totalUsed = parseFloat(d.totalUsed) || 0;
|
||||
@@ -520,6 +576,10 @@
|
||||
data-field="carryover"
|
||||
onchange="updateField(${w.user_id}, 'carryover', this.value)"
|
||||
onblur="var n=parseFloat(this.value||0);this.value=n%1===0?n.toString():n.toFixed(2)">
|
||||
${carryoverExpired && carryover > 0 ? `<div style="font-size:10px;margin-top:2px;line-height:1.2">
|
||||
<span style="color:#10b981">소진${fmtNum(carryoverUsed)}</span>
|
||||
${carryoverLapsed > 0 ? `<span style="color:#9ca3af;text-decoration:line-through;margin-left:3px">만료${fmtNum(carryoverLapsed)}</span>` : ''}
|
||||
</div>` : ''}
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" class="num-input"
|
||||
|
||||
Reference in New Issue
Block a user