feat: [leave-promotions] 통지서 미리보기 모달 기능 추가

- 1차/2차 통지 눈 아이콘 클릭 시 통지서 문서 모달 팝업
- 결재 상태 뱃지, 발송일시, 인쇄, 결재문서 링크 포함
This commit is contained in:
김보곤
2026-03-10 00:44:32 +09:00
parent 60669ffdd5
commit 55019fdf1e

View File

@@ -149,18 +149,32 @@ class="employee-checkbox rounded border-gray-300 text-blue-600"
</td>
<td class="px-3 py-2 text-center text-xs">
@if ($candidate->first_notice)
<a href="{{ route('approvals.show', $candidate->first_notice->id) }}" class="text-blue-600 hover:underline">
{{ $candidate->first_notice->created_at->format('m/d') }}
</a>
<button type="button"
onclick='openNoticePreview(@json($candidate->first_notice->id), "1st", @json($candidate->first_notice->content), "{{ $candidate->first_notice->status }}", "{{ $candidate->first_notice->created_at->format("Y-m-d H:i") }}")'
class="inline-flex items-center gap-1 px-2 py-1 rounded-lg bg-orange-50 text-orange-600 hover:bg-orange-100 transition-colors"
title="1차 통지서 보기">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
</svg>
<span>{{ $candidate->first_notice->created_at->format('m/d') }}</span>
</button>
@else
<span class="text-gray-400">-</span>
@endif
</td>
<td class="px-3 py-2 text-center text-xs">
@if ($candidate->second_notice)
<a href="{{ route('approvals.show', $candidate->second_notice->id) }}" class="text-blue-600 hover:underline">
{{ $candidate->second_notice->created_at->format('m/d') }}
</a>
<button type="button"
onclick='openNoticePreview(@json($candidate->second_notice->id), "2nd", @json($candidate->second_notice->content), "{{ $candidate->second_notice->status }}", "{{ $candidate->second_notice->created_at->format("Y-m-d H:i") }}")'
class="inline-flex items-center gap-1 px-2 py-1 rounded-lg bg-green-50 text-green-600 hover:bg-green-100 transition-colors"
title="2차 통지서 보기">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
</svg>
<span>{{ $candidate->second_notice->created_at->format('m/d') }}</span>
</button>
@else
<span class="text-gray-400">-</span>
@endif
@@ -237,6 +251,47 @@ class="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700">
</div>
</div>
</div>
{{-- 통지서 미리보기 모달 --}}
<div id="notice-preview-modal" class="fixed inset-0 z-50 hidden">
<div class="absolute inset-0 bg-black/50" onclick="closeNoticePreview()"></div>
<div class="absolute inset-0 flex items-center justify-center p-4">
<div class="bg-white rounded-2xl shadow-xl w-full relative" style="max-width: 780px;" onclick="event.stopPropagation()">
<div class="flex items-center justify-between px-5 py-3 border-b border-gray-200 bg-gray-50 rounded-t-2xl">
<div class="flex items-center gap-3">
<h3 id="notice-preview-title" class="text-base font-semibold text-gray-800">통지서 미리보기</h3>
<span id="notice-preview-status" class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium"></span>
<span id="notice-preview-date" class="text-xs text-gray-500"></span>
</div>
<div class="flex items-center gap-2">
<a id="notice-preview-link" href="#" target="_blank"
class="px-3 py-1.5 bg-white border border-gray-300 hover:bg-gray-50 rounded-lg text-xs font-medium transition inline-flex items-center gap-1">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
</svg>
결재문서
</a>
<button type="button" onclick="printNoticePreview()"
class="px-3 py-1.5 bg-white border border-gray-300 hover:bg-gray-50 rounded-lg text-xs font-medium transition inline-flex items-center gap-1">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"/>
</svg>
인쇄
</button>
<button type="button" onclick="closeNoticePreview()"
class="p-1 text-gray-400 hover:text-gray-600 transition">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
</div>
<div class="overflow-y-auto" style="max-height: 80vh;">
<div id="notice-preview-content" style="padding: 40px 48px; font-family: 'Pretendard', 'Malgun Gothic', sans-serif;"></div>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
@@ -397,5 +452,149 @@ function submitBulkNotice() {
btn.textContent = '발송';
});
}
// ── 통지서 미리보기 ──
function buildNotice1PreviewHtml(data) {
return '<div style="text-align:center;margin-bottom:32px;">' +
'<h1 style="font-size:20px;font-weight:800;margin:0;letter-spacing:4px;">연차유급휴가 사용촉진 통지서 (1차)</h1>' +
'</div>' +
'<div style="border-bottom:2px solid #333;padding-bottom:12px;margin-bottom:24px;font-size:13px;line-height:2;">' +
'<div><span style="display:inline-block;width:60px;font-weight:600;">수신</span> : ' + (data.employee_name || '') + '</div>' +
'<div><span style="display:inline-block;width:60px;font-weight:600;">부서</span> : ' + (data.department || '') + '</div>' +
'<div><span style="display:inline-block;width:60px;font-weight:600;">직급</span> : ' + (data.position || '') + '</div>' +
'</div>' +
'<div style="font-size:13px;line-height:1.8;margin-bottom:24px;">' +
'<p>근로기준법 제61조에 따라 귀하의 미사용 연차유급휴가 사용을 촉진하고자 아래와 같이 통지합니다.</p>' +
'</div>' +
'<div style="margin:24px 0;padding:16px 20px;border:1px solid #ddd;border-radius:4px;">' +
'<div style="font-size:13px;font-weight:700;margin-bottom:8px;">■ 연차 현황</div>' +
'<table style="width:100%;border-collapse:collapse;font-size:13px;">' +
'<tr><td style="padding:6px 12px;border:1px solid #ddd;background:#f8f9fa;font-weight:600;width:100px;">발생연차</td>' +
'<td style="padding:6px 12px;border:1px solid #ddd;text-align:center;">' + (data.total_days || 0) + '일</td>' +
'<td style="padding:6px 12px;border:1px solid #ddd;background:#f8f9fa;font-weight:600;width:100px;">사용연차</td>' +
'<td style="padding:6px 12px;border:1px solid #ddd;text-align:center;">' + (data.used_days || 0) + '일</td>' +
'<td style="padding:6px 12px;border:1px solid #ddd;background:#f8f9fa;font-weight:600;width:100px;">잔여연차</td>' +
'<td style="padding:6px 12px;border:1px solid #ddd;text-align:center;color:#dc2626;font-weight:700;">' + (data.remaining_days || 0) + '일</td></tr>' +
'</table>' +
'</div>' +
'<div style="font-size:13px;line-height:1.8;margin-bottom:16px;">' +
'<p>위 잔여 연차휴가에 대하여 아래 기한까지 사용 시기를 지정하여 제출하여 주시기 바랍니다.</p>' +
'</div>' +
'<div style="margin:16px 0;padding:12px 20px;border:1px solid #ddd;border-radius:4px;background:#fffbeb;">' +
'<div style="font-size:13px;font-weight:700;">■ 사용계획 제출기한 : ' + (data.deadline || '') + '</div>' +
'</div>' +
'<div style="font-size:12px;line-height:1.8;margin:24px 0;padding:12px 16px;background:#f8f9fa;border-radius:4px;color:#666;">' +
'<p>기한 내 사용 시기를 제출하지 않을 경우 회사는 근로기준법 제61조에 따라 연차휴가 사용 시기를 지정할 수 있습니다.</p>' +
'<p>본 통지서는 연차 사용 촉진 절차에 따른 법적 통보 문서이며, 확인 시 수신 확인으로 간주됩니다.</p>' +
'</div>' +
'<div style="text-align:center;margin:40px 0 16px;font-size:14px;">' +
'<span>' + (data.company_name || '') + '</span>&nbsp;&nbsp;&nbsp;&nbsp;' +
'<span>대표이사&nbsp;&nbsp;&nbsp;' + (data.ceo_name || '') + '</span>&nbsp;&nbsp;' +
'<span style="color:#999;">[직인날인]</span>' +
'</div>' +
'<div style="border-top:2px solid #333;padding-top:12px;margin-top:24px;">' +
'<div style="font-size:12px;color:#666;text-align:center;">□ 본인은 위 내용을 확인하였으며 연차 사용 시기를 제출하겠습니다.</div>' +
'<div style="text-align:right;margin-top:12px;font-size:12px;color:#999;">서명: ________________________&nbsp;&nbsp;&nbsp;일자: ____년 ____월 ____일</div>' +
'</div>';
}
function buildNotice2PreviewHtml(data) {
let datesHtml = '';
if (data.designated_dates && data.designated_dates.length > 0) {
datesHtml = '<table style="width:100%;border-collapse:collapse;font-size:13px;">';
datesHtml += '<tr><th style="padding:6px 12px;border:1px solid #ddd;background:#f8f9fa;width:60px;">순번</th>' +
'<th style="padding:6px 12px;border:1px solid #ddd;background:#f8f9fa;">지정 휴가일</th></tr>';
data.designated_dates.forEach(function(d, i) {
datesHtml += '<tr><td style="padding:6px 12px;border:1px solid #ddd;text-align:center;">' + (i + 1) + '</td>' +
'<td style="padding:6px 12px;border:1px solid #ddd;text-align:center;">' + d + '</td></tr>';
});
datesHtml += '</table>';
}
return '<div style="text-align:center;margin-bottom:32px;">' +
'<h1 style="font-size:20px;font-weight:800;margin:0;letter-spacing:4px;">연차유급휴가 사용촉진 통지서 (2차)</h1>' +
'</div>' +
'<div style="border-bottom:2px solid #333;padding-bottom:12px;margin-bottom:24px;font-size:13px;line-height:2;">' +
'<div><span style="display:inline-block;width:60px;font-weight:600;">수신</span> : ' + (data.employee_name || '') + '</div>' +
'<div><span style="display:inline-block;width:60px;font-weight:600;">부서</span> : ' + (data.department || '') + '</div>' +
'<div><span style="display:inline-block;width:60px;font-weight:600;">직급</span> : ' + (data.position || '') + '</div>' +
'</div>' +
'<div style="font-size:13px;line-height:1.8;margin-bottom:24px;">' +
'<p>귀하는 연차 사용촉진 1차 통보 이후에도 연차 사용 시기를 제출하지 않아 근로기준법 제61조에 따라 회사가 다음과 같이 휴가 사용일을 지정합니다.</p>' +
'</div>' +
'<div style="margin:24px 0;padding:16px 20px;border:1px solid #ddd;border-radius:4px;">' +
'<div style="font-size:13px;font-weight:700;margin-bottom:8px;">■ 연차 현황 : 잔여 연차 <span style="color:#dc2626;">' + (data.remaining_days || 0) + '</span>일</div>' +
'</div>' +
'<div style="margin:24px 0;padding:16px 20px;border:1px solid #dc2626;border-radius:4px;background:#fef2f2;">' +
'<div style="font-size:13px;font-weight:700;margin-bottom:12px;">■ 회사 지정 휴가일</div>' +
datesHtml +
'</div>' +
'<div style="font-size:13px;line-height:1.8;margin-bottom:16px;">' +
'<p>위 지정된 날짜에 연차휴가를 사용하여 주시기 바랍니다.</p>' +
'</div>' +
'<div style="font-size:12px;line-height:1.8;margin:24px 0;padding:12px 16px;background:#f8f9fa;border-radius:4px;color:#666;">' +
'<p>본 통지서는 근로기준법 제61조에 따른 연차 사용촉진 절차에 의한 통보입니다.</p>' +
'</div>' +
'<div style="text-align:center;margin:40px 0 16px;font-size:14px;">' +
'<span>' + (data.company_name || '') + '</span>&nbsp;&nbsp;&nbsp;&nbsp;' +
'<span>대표이사&nbsp;&nbsp;&nbsp;' + (data.ceo_name || '') + '</span>&nbsp;&nbsp;' +
'<span style="color:#999;">[직인날인]</span>' +
'</div>' +
'<div style="border-top:2px solid #333;padding-top:12px;margin-top:24px;">' +
'<div style="font-size:12px;color:#666;text-align:center;">□ 본인은 위 내용을 확인하였습니다.</div>' +
'<div style="text-align:right;margin-top:12px;font-size:12px;color:#999;">서명: ________________________&nbsp;&nbsp;&nbsp;일자: ____년 ____월 ____일</div>' +
'</div>';
}
const statusMap = {
draft: { label: '임시저장', bg: 'bg-gray-100', text: 'text-gray-600' },
pending: { label: '결재중', bg: 'bg-blue-100', text: 'text-blue-700' },
approved: { label: '승인', bg: 'bg-green-100', text: 'text-green-700' },
rejected: { label: '반려', bg: 'bg-red-100', text: 'text-red-700' },
cancelled: { label: '취소', bg: 'bg-gray-100', text: 'text-gray-500' },
};
function openNoticePreview(id, type, content, status, createdAt) {
const data = typeof content === 'string' ? JSON.parse(content) : content;
const modal = document.getElementById('notice-preview-modal');
const titleEl = document.getElementById('notice-preview-title');
const statusEl = document.getElementById('notice-preview-status');
const dateEl = document.getElementById('notice-preview-date');
const contentEl = document.getElementById('notice-preview-content');
const linkEl = document.getElementById('notice-preview-link');
// 제목
titleEl.textContent = type === '1st' ? '1차 통지서 미리보기' : '2차 통지서 미리보기';
// 상태 뱃지
const st = statusMap[status] || { label: status, bg: 'bg-gray-100', text: 'text-gray-600' };
statusEl.textContent = st.label;
statusEl.className = 'inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ' + st.bg + ' ' + st.text;
// 날짜
dateEl.textContent = createdAt;
// 결재문서 링크
linkEl.href = '/approvals/' + id;
// 문서 미리보기 HTML
contentEl.innerHTML = type === '1st' ? buildNotice1PreviewHtml(data) : buildNotice2PreviewHtml(data);
modal.classList.remove('hidden');
}
function closeNoticePreview() {
document.getElementById('notice-preview-modal').classList.add('hidden');
}
function printNoticePreview() {
const content = document.getElementById('notice-preview-content').innerHTML;
const title = document.getElementById('notice-preview-title').textContent;
const win = window.open('', '_blank');
win.document.write('<html><head><title>' + title + '</title><style>body{font-family:"Pretendard","Malgun Gothic",sans-serif;padding:40px 48px;}@media print{body{padding:20px 30px;}}</style></head><body>' + content + '</body></html>');
win.document.close();
win.print();
}
</script>
@endpush