feat: [approval] 연차사용촉진 통지서 1차/2차 양식 추가

- 1차 통지서: 직원 선택, 연차 현황(발생/사용/잔여), 제출기한, 법적 문구
- 2차 통지서: 직원 선택, 잔여연차, 회사 지정 휴가일(다건), 법적 문구
- create/edit/show 통합 완료
- 미리보기/인쇄 기능 포함
This commit is contained in:
김보곤
2026-03-07 00:28:58 +09:00
parent fc5af2734a
commit 2a1e72a15e
7 changed files with 1001 additions and 5 deletions

View File

@@ -150,6 +150,18 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-
'tenantInfo' => $tenantInfo ?? [],
])
{{-- 연차사용촉진 1 통지서 --}}
@include('approvals.partials._leave-promotion-1st-form', [
'tenantInfo' => $tenantInfo ?? [],
'employees' => $employees ?? collect(),
])
{{-- 연차사용촉진 2 통지서 --}}
@include('approvals.partials._leave-promotion-2nd-form', [
'tenantInfo' => $tenantInfo ?? [],
'employees' => $employees ?? collect(),
])
{{-- 견적서 전용 --}}
@include('approvals.partials._quotation-form', [
'tenantInfo' => $tenantInfo ?? [],
@@ -349,6 +361,16 @@ class="p-1 text-gray-400 hover:text-gray-600 transition">
color: 'border-slate-300 bg-slate-50', titleColor: 'text-slate-800', textColor: 'text-slate-600',
text: '이사회 개최 내용을 기록하는 공식 문서입니다. 일시, 장소, 출석현황, 의안, 의사경과, 기명날인 등을 기재하며, 법적 효력을 갖는 회의록입니다.',
},
leave_promotion_1st: {
title: '연차사용촉진 통지서 (1차)', icon: '📅',
color: 'border-orange-200 bg-orange-50', titleColor: 'text-orange-800', textColor: 'text-orange-600',
text: '근로기준법 제61조에 따른 연차 사용 촉진 1차 통지서입니다. 잔여 연차 현황을 안내하고, 사용계획 제출기한을 지정하여 직원에게 통보합니다.',
},
leave_promotion_2nd: {
title: '연차사용촉진 통지서 (2차)', icon: '🚨',
color: 'border-red-200 bg-red-50', titleColor: 'text-red-800', textColor: 'text-red-600',
text: '1차 통지 후에도 사용 시기를 제출하지 않은 직원에게 회사가 휴가일을 지정하는 2차 통지서입니다. 근로기준법 제61조에 따른 법적 효력이 있습니다.',
},
quotation: {
title: '견적서', icon: '💵',
color: 'border-green-200 bg-green-50', titleColor: 'text-green-800', textColor: 'text-green-600',
@@ -384,7 +406,7 @@ class="p-1 text-gray-400 hover:text-gray-600 transition">
// 2단계 분류 정의 (코드 → 카테고리)
const formCategoryMap = {
BUSINESS_DRAFT: '일반', official_letter: '일반',
leave: '인사/근태', attendance_request: '인사/근태', resignation: '인사/근태', reason_report: '인사/근태', delegation: '인사/근태', board_minutes: '인사/근태',
leave: '인사/근태', attendance_request: '인사/근태', resignation: '인사/근태', reason_report: '인사/근태', delegation: '인사/근태', board_minutes: '인사/근태', leave_promotion_1st: '인사/근태', leave_promotion_2nd: '인사/근태',
employment_cert: '증명서', career_cert: '증명서', appointment_cert: '증명서', seal_usage: '증명서',
pr_expense: '품의', pr_contract: '품의', pr_purchase: '품의', pr_trip: '품의', pr_settlement: '품의',
quotation: '재무', expense: '재무',
@@ -476,6 +498,8 @@ function updateFormDescription(formId) {
let isBoardMinutesForm = false;
let isQuotationForm = false;
let isOfficialLetterForm = false;
let isLeavePromotion1stForm = false;
let isLeavePromotion2ndForm = false;
// 양식코드별 표시할 유형 목록
const leaveTypesByFormCode = {
@@ -729,6 +753,8 @@ function switchFormMode(formId) {
const boardMinutesContainer = document.getElementById('board-minutes-form-container');
const quotationContainer = document.getElementById('quotation-form-container');
const officialLetterContainer = document.getElementById('official-letter-form-container');
const lp1Container = document.getElementById('leave-promotion-1st-form-container');
const lp2Container = document.getElementById('leave-promotion-2nd-form-container');
const bodyArea = document.getElementById('body-area');
const expenseLoadBtn = document.getElementById('expense-load-btn');
@@ -747,6 +773,8 @@ function switchFormMode(formId) {
boardMinutesContainer.style.display = 'none';
quotationContainer.style.display = 'none';
officialLetterContainer.style.display = 'none';
lp1Container.style.display = 'none';
lp2Container.style.display = 'none';
expenseLoadBtn.style.display = 'none';
bodyArea.style.display = 'none';
isExpenseForm = false;
@@ -761,6 +789,8 @@ function switchFormMode(formId) {
isBoardMinutesForm = false;
isQuotationForm = false;
isOfficialLetterForm = false;
isLeavePromotion1stForm = false;
isLeavePromotion2ndForm = false;
if (code === 'expense') {
isExpenseForm = true;
@@ -835,6 +865,12 @@ function switchFormMode(formId) {
} else if (code === 'official_letter') {
isOfficialLetterForm = true;
officialLetterContainer.style.display = '';
} else if (code === 'leave_promotion_1st') {
isLeavePromotion1stForm = true;
lp1Container.style.display = '';
} else if (code === 'leave_promotion_2nd') {
isLeavePromotion2ndForm = true;
lp2Container.style.display = '';
} else {
bodyArea.style.display = '';
}
@@ -845,7 +881,7 @@ function applyBodyTemplate(formId) {
switchFormMode(formId);
// 전용 폼이면 제목을 양식명으로 설정하고 body template 적용 건너뜀
if (isExpenseForm || isPurchaseRequestForm || isLeaveForm || isCertForm || isCareerCertForm || isAppointmentCertForm || isResignationForm || isSealUsageForm || isDelegationForm || isBoardMinutesForm || isQuotationForm || isOfficialLetterForm) {
if (isExpenseForm || isPurchaseRequestForm || isLeaveForm || isCertForm || isCareerCertForm || isAppointmentCertForm || isResignationForm || isSealUsageForm || isDelegationForm || isBoardMinutesForm || isQuotationForm || isOfficialLetterForm || isLeavePromotion1stForm || isLeavePromotion2ndForm) {
const titleEl = document.getElementById('title');
const formSelect = document.getElementById('form_id');
titleEl.value = formSelect.options[formSelect.selectedIndex].text;
@@ -1181,6 +1217,22 @@ function applyBodyTemplate(formId) {
email: document.getElementById('ol-email-input')?.value || document.getElementById('ol-email').value,
};
formBody = null;
} else if (isLeavePromotion1stForm) {
const lp1UserId = document.getElementById('lp1-user-id').value;
if (!lp1UserId) { showToast('대상 직원을 선택해주세요.', 'warning'); return; }
const lp1Deadline = document.getElementById('lp1-deadline').value;
if (!lp1Deadline) { showToast('사용계획 제출기한을 입력해주세요.', 'warning'); return; }
formContent = getLp1Data();
formBody = null;
} else if (isLeavePromotion2ndForm) {
const lp2UserId = document.getElementById('lp2-user-id').value;
if (!lp2UserId) { showToast('대상 직원을 선택해주세요.', 'warning'); return; }
const lp2Dates = getLp2Dates();
if (lp2Dates.length === 0) { showToast('지정 휴가일을 1건 이상 입력해주세요.', 'warning'); return; }
formContent = getLp2Data();
formBody = null;
} else if (isDelegationForm) {
const dlAgentName = document.getElementById('dl-agent-name').value.trim();
if (!dlAgentName) {

View File

@@ -168,6 +168,18 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-
'tenantInfo' => $tenantInfo ?? [],
])
{{-- 연차사용촉진 1 통지서 --}}
@include('approvals.partials._leave-promotion-1st-form', [
'tenantInfo' => $tenantInfo ?? [],
'employees' => $employees ?? collect(),
])
{{-- 연차사용촉진 2 통지서 --}}
@include('approvals.partials._leave-promotion-2nd-form', [
'tenantInfo' => $tenantInfo ?? [],
'employees' => $employees ?? collect(),
])
{{-- 견적서 전용 --}}
@include('approvals.partials._quotation-form', [
'tenantInfo' => $tenantInfo ?? [],
@@ -314,6 +326,8 @@ class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm fon
let isBoardMinutesForm = false;
let isQuotationForm = false;
let isOfficialLetterForm = false;
let isLeavePromotion1stForm = false;
let isLeavePromotion2ndForm = false;
const formDescriptions = {
BUSINESS_DRAFT: {
@@ -381,6 +395,16 @@ class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm fon
color: 'border-slate-300 bg-slate-50', titleColor: 'text-slate-800', textColor: 'text-slate-600',
text: '이사회 개최 내용을 기록하는 공식 문서입니다. 일시, 장소, 출석현황, 의안, 의사경과, 기명날인 등을 기재하며, 법적 효력을 갖는 회의록입니다.',
},
leave_promotion_1st: {
title: '연차사용촉진 통지서 (1차)', icon: '📅',
color: 'border-orange-200 bg-orange-50', titleColor: 'text-orange-800', textColor: 'text-orange-600',
text: '근로기준법 제61조에 따른 연차 사용 촉진 1차 통지서입니다. 잔여 연차 현황을 안내하고, 사용계획 제출기한을 지정하여 직원에게 통보합니다.',
},
leave_promotion_2nd: {
title: '연차사용촉진 통지서 (2차)', icon: '🚨',
color: 'border-red-200 bg-red-50', titleColor: 'text-red-800', textColor: 'text-red-600',
text: '1차 통지 후에도 사용 시기를 제출하지 않은 직원에게 회사가 휴가일을 지정하는 2차 통지서입니다. 근로기준법 제61조에 따른 법적 효력이 있습니다.',
},
quotation: {
title: '견적서', icon: '💰',
color: 'border-emerald-200 bg-emerald-50', titleColor: 'text-emerald-800', textColor: 'text-emerald-600',
@@ -416,7 +440,7 @@ class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm fon
// 2단계 분류 정의
const formCategoryMap = {
BUSINESS_DRAFT: '일반', official_letter: '일반',
leave: '인사/근태', attendance_request: '인사/근태', resignation: '인사/근태', reason_report: '인사/근태', delegation: '인사/근태', board_minutes: '인사/근태',
leave: '인사/근태', attendance_request: '인사/근태', resignation: '인사/근태', reason_report: '인사/근태', delegation: '인사/근태', board_minutes: '인사/근태', leave_promotion_1st: '인사/근태', leave_promotion_2nd: '인사/근태',
employment_cert: '증명서', career_cert: '증명서', appointment_cert: '증명서', seal_usage: '증명서',
pr_expense: '품의', pr_contract: '품의', pr_purchase: '품의', pr_trip: '품의', pr_settlement: '품의',
expense: '재무', quotation: '재무',
@@ -661,6 +685,8 @@ function switchFormMode(formId) {
const boardMinutesContainer = document.getElementById('board-minutes-form-container');
const quotationContainer = document.getElementById('quotation-form-container');
const officialLetterContainer = document.getElementById('official-letter-form-container');
const lp1Container = document.getElementById('leave-promotion-1st-form-container');
const lp2Container = document.getElementById('leave-promotion-2nd-form-container');
const bodyArea = document.getElementById('body-area');
expenseContainer.style.display = 'none';
@@ -671,6 +697,8 @@ function switchFormMode(formId) {
boardMinutesContainer.style.display = 'none';
quotationContainer.style.display = 'none';
officialLetterContainer.style.display = 'none';
lp1Container.style.display = 'none';
lp2Container.style.display = 'none';
bodyArea.style.display = 'none';
isExpenseForm = false;
isPurchaseRequestForm = false;
@@ -680,6 +708,8 @@ function switchFormMode(formId) {
isBoardMinutesForm = false;
isQuotationForm = false;
isOfficialLetterForm = false;
isLeavePromotion1stForm = false;
isLeavePromotion2ndForm = false;
if (code === 'expense') {
isExpenseForm = true;
@@ -711,6 +741,12 @@ function switchFormMode(formId) {
} else if (code === 'official_letter') {
isOfficialLetterForm = true;
officialLetterContainer.style.display = '';
} else if (code === 'leave_promotion_1st') {
isLeavePromotion1stForm = true;
lp1Container.style.display = '';
} else if (code === 'leave_promotion_2nd') {
isLeavePromotion2ndForm = true;
lp2Container.style.display = '';
} else {
bodyArea.style.display = '';
}
@@ -721,7 +757,7 @@ function applyBodyTemplate(formId) {
switchFormMode(formId);
// 전용 폼이면 제목만 자동 설정하고 body template 적용 건너뜀
if (isExpenseForm || isPurchaseRequestForm || isCertForm || isSealUsageForm || isDelegationForm || isBoardMinutesForm || isQuotationForm || isOfficialLetterForm) {
if (isExpenseForm || isPurchaseRequestForm || isCertForm || isSealUsageForm || isDelegationForm || isBoardMinutesForm || isQuotationForm || isOfficialLetterForm || isLeavePromotion1stForm || isLeavePromotion2ndForm) {
const titleEl = document.getElementById('title');
if (!titleEl.value.trim()) {
const formSelect = document.getElementById('form_id');
@@ -916,8 +952,39 @@ function applyBodyTemplate(formId) {
}
}
// 연차사용촉진 1차 통지서 기존 데이터 복원
if (isLeavePromotion1stForm) {
const lp1Content = @json($approval->content ?? []);
if (lp1Content.employee_id) {
document.getElementById('lp1-user-id').value = lp1Content.employee_id || '';
document.getElementById('lp1-department').value = lp1Content.department || '';
document.getElementById('lp1-position').value = lp1Content.position || '';
document.getElementById('lp1-total-days').value = lp1Content.total_days || 0;
document.getElementById('lp1-used-days').value = lp1Content.used_days || 0;
document.getElementById('lp1-remaining-days').value = lp1Content.remaining_days || 0;
document.getElementById('lp1-deadline').value = lp1Content.deadline || '';
}
}
// 연차사용촉진 2차 통지서 기존 데이터 복원
if (isLeavePromotion2ndForm) {
const lp2Content = @json($approval->content ?? []);
if (lp2Content.employee_id) {
document.getElementById('lp2-user-id').value = lp2Content.employee_id || '';
document.getElementById('lp2-department').value = lp2Content.department || '';
document.getElementById('lp2-position').value = lp2Content.position || '';
document.getElementById('lp2-remaining-days').value = lp2Content.remaining_days || 0;
if (lp2Content.designated_dates && lp2Content.designated_dates.length > 0) {
const lp2Alpine = document.getElementById('leave-promotion-2nd-form-container')._x_dataStack?.[0];
if (lp2Alpine) {
lp2Alpine.dates = lp2Content.designated_dates.map(d => ({ date: d }));
}
}
}
}
// 전용 폼이 아닌 경우에만 Quill 편집기 자동 활성화
if (!isExpenseForm && !isPurchaseRequestForm && !isCertForm && !isSealUsageForm && !isDelegationForm && !isBoardMinutesForm && !isQuotationForm && !isOfficialLetterForm) {
if (!isExpenseForm && !isPurchaseRequestForm && !isCertForm && !isSealUsageForm && !isDelegationForm && !isBoardMinutesForm && !isQuotationForm && !isOfficialLetterForm && !isLeavePromotion1stForm && !isLeavePromotion2ndForm) {
const existingBody = document.getElementById('body').value;
if (/<[a-z][\s\S]*>/i.test(existingBody)) {
document.getElementById('useEditor').checked = true;
@@ -1100,6 +1167,20 @@ function applyBodyTemplate(formId) {
email: document.getElementById('ol-email-input')?.value || document.getElementById('ol-email').value,
};
formBody = null;
} else if (isLeavePromotion1stForm) {
const lp1UserId = document.getElementById('lp1-user-id').value;
if (!lp1UserId) { showToast('대상 직원을 선택해주세요.', 'warning'); return; }
const lp1Deadline = document.getElementById('lp1-deadline').value;
if (!lp1Deadline) { showToast('사용계획 제출기한을 입력해주세요.', 'warning'); return; }
formContent = getLp1Data();
formBody = null;
} else if (isLeavePromotion2ndForm) {
const lp2UserId = document.getElementById('lp2-user-id').value;
if (!lp2UserId) { showToast('대상 직원을 선택해주세요.', 'warning'); return; }
const lp2Dates = getLp2Dates();
if (lp2Dates.length === 0) { showToast('지정 휴가일을 1건 이상 입력해주세요.', 'warning'); return; }
formContent = getLp2Data();
formBody = null;
} else if (isDelegationForm) {
const dlAgentName = document.getElementById('dl-agent-name').value.trim();
if (!dlAgentName) {

View File

@@ -0,0 +1,253 @@
{{--
연차유급휴가 사용촉진 통지서 (1) 전용
Props:
$tenantInfo (array) - 테넌트(회사) 정보
$employees (Collection) - 직원 목록
--}}
@php
$tenantInfo = $tenantInfo ?? [];
$employees = $employees ?? collect();
@endphp
<div id="leave-promotion-1st-form-container" style="display: none;" class="mb-4">
<input type="hidden" id="lp1-company-name" value="{{ $tenantInfo['company_name'] ?? '' }}">
<input type="hidden" id="lp1-ceo-name" value="{{ $tenantInfo['ceo_name'] ?? '' }}">
<div class="space-y-4">
{{-- 1. 수신자 정보 --}}
<div class="border border-gray-200 rounded-lg overflow-hidden">
<div class="bg-gray-50 px-4 py-2 border-b border-gray-200">
<h3 class="text-sm font-semibold text-gray-700">1. 수신자 정보</h3>
</div>
<div class="p-4 space-y-3">
<div class="flex gap-4 flex-wrap">
<div style="flex: 1 1 300px; max-width: 400px;">
<label class="block text-xs font-medium text-gray-500 mb-1">대상 직원 <span class="text-red-500">*</span></label>
<select id="lp1-user-id" onchange="loadLp1EmployeeInfo(this.value)"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="">-- 직원 선택 --</option>
@foreach($employees as $emp)
<option value="{{ $emp->id }}"
data-department="{{ $emp->departments->first()?->name ?? '' }}"
data-position="{{ $emp->position ?? '' }}">
{{ $emp->name }} {{ $emp->departments->first()?->name ? '('.$emp->departments->first()->name.')' : '' }}
</option>
@endforeach
</select>
</div>
</div>
<div class="flex gap-4 flex-wrap">
<div style="flex: 1 1 200px; max-width: 250px;">
<label class="block text-xs font-medium text-gray-500 mb-1">부서</label>
<input type="text" id="lp1-department" readonly
class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm bg-gray-50 text-gray-700">
</div>
<div style="flex: 1 1 150px; max-width: 200px;">
<label class="block text-xs font-medium text-gray-500 mb-1">직급</label>
<input type="text" id="lp1-position" readonly
class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm bg-gray-50 text-gray-700">
</div>
</div>
</div>
</div>
{{-- 2. 연차 현황 --}}
<div class="border border-gray-200 rounded-lg overflow-hidden">
<div class="bg-gray-50 px-4 py-2 border-b border-gray-200">
<h3 class="text-sm font-semibold text-gray-700">2. 연차 현황</h3>
</div>
<div class="p-4">
<div class="flex gap-4 flex-wrap">
<div style="flex: 0 0 120px;">
<label class="block text-xs font-medium text-gray-500 mb-1">발생연차 <span class="text-red-500">*</span></label>
<input type="number" id="lp1-total-days" min="0" step="0.5" value="15"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 text-center">
</div>
<div style="flex: 0 0 120px;">
<label class="block text-xs font-medium text-gray-500 mb-1">사용연차</label>
<input type="number" id="lp1-used-days" min="0" step="0.5" value="5"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 text-center">
</div>
<div style="flex: 0 0 120px;">
<label class="block text-xs font-medium text-gray-500 mb-1">잔여연차</label>
<input type="number" id="lp1-remaining-days" readonly
class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm bg-amber-50 text-amber-800 font-semibold text-center">
</div>
</div>
</div>
</div>
{{-- 3. 사용계획 제출기한 --}}
<div class="border border-gray-200 rounded-lg overflow-hidden">
<div class="bg-gray-50 px-4 py-2 border-b border-gray-200">
<h3 class="text-sm font-semibold text-gray-700">3. 사용계획 제출기한</h3>
</div>
<div class="p-4">
<div style="max-width: 250px;">
<label class="block text-xs font-medium text-gray-500 mb-1">제출기한 <span class="text-red-500">*</span></label>
<input type="date" id="lp1-deadline"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<p class="text-xs text-gray-400 mt-2">* 기한 사용 시기를 제출하지 않을 경우 회사가 연차 사용 시기를 지정할 있습니다.</p>
</div>
</div>
{{-- 4. 통지 내용 (법적 문구) --}}
<div class="border border-blue-200 rounded-lg overflow-hidden">
<div class="bg-blue-50 px-4 py-2 border-b border-blue-200">
<h3 class="text-sm font-semibold text-blue-700">4. 법적 통지 문구</h3>
</div>
<div class="p-4">
<div class="text-xs text-gray-600 leading-relaxed space-y-2 bg-gray-50 p-3 rounded border border-gray-200">
<p>근로기준법 제61조에 따라 귀하의 미사용 연차유급휴가 사용을 촉진하고자 아래와 같이 통지합니다.</p>
<p> 잔여 연차휴가에 대하여 기한까지 사용 시기를 지정하여 제출하여 주시기 바랍니다.</p>
<p class="text-red-600 font-medium">기한 사용 시기를 제출하지 않을 경우 회사는 근로기준법 제61조에 따라 연차휴가 사용 시기를 지정할 있습니다.</p>
<p> 통지서는 연차 사용 촉진 절차에 따른 법적 통보 문서이며, 확인 수신 확인으로 간주됩니다.</p>
</div>
</div>
</div>
{{-- 미리보기 버튼 --}}
<div class="flex justify-end">
<button type="button" onclick="openLp1Preview()"
class="px-3 py-2 bg-indigo-50 text-indigo-600 hover:bg-indigo-100 border border-indigo-200 rounded-lg text-sm font-medium transition inline-flex items-center gap-1">
<svg class="w-4 h-4" 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>
미리보기
</button>
</div>
</div>
</div>
{{-- 1 통지서 미리보기 모달 --}}
<div id="lp1-preview-modal" style="display: none;" class="fixed inset-0 z-50">
<div class="absolute inset-0 bg-black/50" onclick="closeLp1Preview()"></div>
<div class="relative flex items-center justify-center min-h-full p-4">
<div class="bg-white rounded-xl shadow-2xl w-full overflow-hidden relative" style="max-width: 780px;">
<div class="flex items-center justify-between px-5 py-3 border-b border-gray-200 bg-gray-50">
<h3 class="text-base font-semibold text-gray-800">1 통지서 미리보기</h3>
<div class="flex items-center gap-2">
<button type="button" onclick="printLp1Preview()"
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="closeLp1Preview()"
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="lp1-preview-content" style="padding: 40px 48px; font-family: 'Pretendard', 'Malgun Gothic', sans-serif;"></div>
</div>
</div>
</div>
</div>
@push('scripts')
<script>
function loadLp1EmployeeInfo(userId) {
const sel = document.getElementById('lp1-user-id');
const opt = sel.options[sel.selectedIndex];
document.getElementById('lp1-department').value = opt?.dataset?.department || '';
document.getElementById('lp1-position').value = opt?.dataset?.position || '';
}
// 잔여연차 자동 계산
['lp1-total-days', 'lp1-used-days'].forEach(id => {
document.getElementById(id)?.addEventListener('input', function() {
const total = parseFloat(document.getElementById('lp1-total-days').value) || 0;
const used = parseFloat(document.getElementById('lp1-used-days').value) || 0;
document.getElementById('lp1-remaining-days').value = Math.max(0, total - used);
});
});
// 초기 계산
document.addEventListener('DOMContentLoaded', () => {
const total = parseFloat(document.getElementById('lp1-total-days')?.value) || 0;
const used = parseFloat(document.getElementById('lp1-used-days')?.value) || 0;
const rem = document.getElementById('lp1-remaining-days');
if (rem) rem.value = Math.max(0, total - used);
});
function buildLp1PreviewHtml(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 getLp1Data() {
const sel = document.getElementById('lp1-user-id');
const opt = sel.options[sel.selectedIndex];
return {
employee_name: opt?.text?.replace(/\s*\(.*\)/, '') || '',
employee_id: sel.value,
department: document.getElementById('lp1-department').value,
position: document.getElementById('lp1-position').value,
total_days: parseFloat(document.getElementById('lp1-total-days').value) || 0,
used_days: parseFloat(document.getElementById('lp1-used-days').value) || 0,
remaining_days: parseFloat(document.getElementById('lp1-remaining-days').value) || 0,
deadline: document.getElementById('lp1-deadline').value,
company_name: document.getElementById('lp1-company-name').value,
ceo_name: document.getElementById('lp1-ceo-name').value,
};
}
function openLp1Preview() {
document.getElementById('lp1-preview-content').innerHTML = buildLp1PreviewHtml(getLp1Data());
document.getElementById('lp1-preview-modal').style.display = '';
}
function closeLp1Preview() { document.getElementById('lp1-preview-modal').style.display = 'none'; }
function printLp1Preview() {
const content = document.getElementById('lp1-preview-content').innerHTML;
const win = window.open('', '_blank');
win.document.write('<html><head><title>연차사용촉진 1차 통지서</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

View File

@@ -0,0 +1,176 @@
{{--
연차유급휴가 사용촉진 통지서 (1) 읽기전용
Props:
$content (array) - approvals.content JSON
--}}
<div class="space-y-4">
{{-- 미리보기 버튼 --}}
<div class="flex justify-end gap-2">
<button type="button" onclick="openLp1ShowPreview()"
class="px-3 py-1.5 bg-indigo-50 text-indigo-600 hover:bg-indigo-100 border border-indigo-200 rounded-lg text-sm font-medium transition inline-flex items-center gap-1">
<svg class="w-4 h-4" 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>
통지서 미리보기
</button>
</div>
{{-- 수신자 정보 --}}
<div class="border border-gray-200 rounded-lg overflow-hidden">
<div class="bg-gray-50 px-4 py-2 border-b border-gray-200">
<h3 class="text-sm font-semibold text-gray-700">수신자 정보</h3>
</div>
<div class="p-4">
<div class="grid gap-3" style="grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));">
<div>
<span class="text-xs text-gray-500">직원명</span>
<div class="text-sm font-medium mt-0.5">{{ $content['employee_name'] ?? '-' }}</div>
</div>
<div>
<span class="text-xs text-gray-500">부서</span>
<div class="text-sm font-medium mt-0.5">{{ $content['department'] ?? '-' }}</div>
</div>
<div>
<span class="text-xs text-gray-500">직급</span>
<div class="text-sm font-medium mt-0.5">{{ $content['position'] ?? '-' }}</div>
</div>
</div>
</div>
</div>
{{-- 연차 현황 --}}
<div class="border border-gray-200 rounded-lg overflow-hidden">
<div class="bg-gray-50 px-4 py-2 border-b border-gray-200">
<h3 class="text-sm font-semibold text-gray-700">연차 현황</h3>
</div>
<div class="p-4">
<div class="grid gap-3" style="grid-template-columns: repeat(3, 1fr);">
<div class="text-center p-3 bg-blue-50 rounded-lg">
<div class="text-xs text-blue-600">발생연차</div>
<div class="text-lg font-bold text-blue-800 mt-1">{{ $content['total_days'] ?? 0 }}</div>
</div>
<div class="text-center p-3 bg-green-50 rounded-lg">
<div class="text-xs text-green-600">사용연차</div>
<div class="text-lg font-bold text-green-800 mt-1">{{ $content['used_days'] ?? 0 }}</div>
</div>
<div class="text-center p-3 bg-amber-50 rounded-lg">
<div class="text-xs text-amber-600">잔여연차</div>
<div class="text-lg font-bold text-amber-800 mt-1">{{ $content['remaining_days'] ?? 0 }}</div>
</div>
</div>
</div>
</div>
{{-- 사용계획 제출기한 --}}
<div class="border border-amber-200 rounded-lg overflow-hidden">
<div class="bg-amber-50 px-4 py-2 border-b border-amber-200">
<h3 class="text-sm font-semibold text-amber-700">사용계획 제출기한</h3>
</div>
<div class="p-4">
<div class="text-base font-semibold text-amber-800">{{ $content['deadline'] ?? '-' }}</div>
</div>
</div>
{{-- 법적 문구 --}}
<div class="border border-blue-200 rounded-lg overflow-hidden">
<div class="bg-blue-50 px-4 py-2 border-b border-blue-200">
<h3 class="text-sm font-semibold text-blue-700">법적 통지 문구</h3>
</div>
<div class="p-4 text-xs text-gray-600 leading-relaxed space-y-1">
<p>근로기준법 제61조에 따라 귀하의 미사용 연차유급휴가 사용을 촉진하고자 통지합니다.</p>
<p class="text-red-600 font-medium">기한 사용 시기를 제출하지 않을 경우 회사는 연차휴가 사용 시기를 지정할 있습니다.</p>
</div>
</div>
</div>
{{-- 미리보기 모달 --}}
<div id="lp1-show-preview-modal" style="display: none;" class="fixed inset-0 z-50">
<div class="absolute inset-0 bg-black/50" onclick="closeLp1ShowPreview()"></div>
<div class="relative flex items-center justify-center min-h-full p-4">
<div class="bg-white rounded-xl shadow-2xl w-full overflow-hidden relative" style="max-width: 780px;">
<div class="flex items-center justify-between px-5 py-3 border-b border-gray-200 bg-gray-50">
<h3 class="text-base font-semibold text-gray-800">1 통지서 미리보기</h3>
<div class="flex items-center gap-2">
<button type="button" onclick="printLp1ShowPreview()"
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="closeLp1ShowPreview()"
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="lp1-show-preview-content" style="padding: 40px 48px; font-family: 'Pretendard', 'Malgun Gothic', sans-serif;"></div>
</div>
</div>
</div>
</div>
@push('scripts')
<script>
function buildLp1ShowPreviewHtml(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 openLp1ShowPreview() {
const data = @json($content);
document.getElementById('lp1-show-preview-content').innerHTML = buildLp1ShowPreviewHtml(data);
document.getElementById('lp1-show-preview-modal').style.display = '';
}
function closeLp1ShowPreview() { document.getElementById('lp1-show-preview-modal').style.display = 'none'; }
function printLp1ShowPreview() {
const content = document.getElementById('lp1-show-preview-content').innerHTML;
const win = window.open('', '_blank');
win.document.write('<html><head><title>연차사용촉진 1차 통지서</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

View File

@@ -0,0 +1,254 @@
{{--
연차유급휴가 사용촉진 통지서 (2) 전용
Props:
$tenantInfo (array) - 테넌트(회사) 정보
$employees (Collection) - 직원 목록
--}}
@php
$tenantInfo = $tenantInfo ?? [];
$employees = $employees ?? collect();
@endphp
<div id="leave-promotion-2nd-form-container" style="display: none;" class="mb-4"
x-data="{
dates: [{ date: '' }],
addDate() { this.dates.push({ date: '' }); },
removeDate(i) { if (this.dates.length > 1) this.dates.splice(i, 1); },
}">
<input type="hidden" id="lp2-company-name" value="{{ $tenantInfo['company_name'] ?? '' }}">
<input type="hidden" id="lp2-ceo-name" value="{{ $tenantInfo['ceo_name'] ?? '' }}">
<div class="space-y-4">
{{-- 1. 수신자 정보 --}}
<div class="border border-gray-200 rounded-lg overflow-hidden">
<div class="bg-gray-50 px-4 py-2 border-b border-gray-200">
<h3 class="text-sm font-semibold text-gray-700">1. 수신자 정보</h3>
</div>
<div class="p-4 space-y-3">
<div class="flex gap-4 flex-wrap">
<div style="flex: 1 1 300px; max-width: 400px;">
<label class="block text-xs font-medium text-gray-500 mb-1">대상 직원 <span class="text-red-500">*</span></label>
<select id="lp2-user-id" onchange="loadLp2EmployeeInfo(this.value)"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="">-- 직원 선택 --</option>
@foreach($employees as $emp)
<option value="{{ $emp->id }}"
data-department="{{ $emp->departments->first()?->name ?? '' }}"
data-position="{{ $emp->position ?? '' }}">
{{ $emp->name }} {{ $emp->departments->first()?->name ? '('.$emp->departments->first()->name.')' : '' }}
</option>
@endforeach
</select>
</div>
</div>
<div class="flex gap-4 flex-wrap">
<div style="flex: 1 1 200px; max-width: 250px;">
<label class="block text-xs font-medium text-gray-500 mb-1">부서</label>
<input type="text" id="lp2-department" readonly
class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm bg-gray-50 text-gray-700">
</div>
<div style="flex: 1 1 150px; max-width: 200px;">
<label class="block text-xs font-medium text-gray-500 mb-1">직급</label>
<input type="text" id="lp2-position" readonly
class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm bg-gray-50 text-gray-700">
</div>
</div>
</div>
</div>
{{-- 2. 잔여 연차 --}}
<div class="border border-gray-200 rounded-lg overflow-hidden">
<div class="bg-gray-50 px-4 py-2 border-b border-gray-200">
<h3 class="text-sm font-semibold text-gray-700">2. 잔여 연차</h3>
</div>
<div class="p-4">
<div style="max-width: 150px;">
<label class="block text-xs font-medium text-gray-500 mb-1">잔여연차 <span class="text-red-500">*</span></label>
<input type="number" id="lp2-remaining-days" min="0" step="0.5" value="10"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 text-center font-semibold text-amber-800 bg-amber-50">
</div>
</div>
</div>
{{-- 3. 회사 지정 휴가일 --}}
<div class="border border-gray-200 rounded-lg overflow-hidden">
<div class="bg-gray-50 px-4 py-2 border-b border-gray-200">
<h3 class="text-sm font-semibold text-gray-700">3. 회사 지정 휴가일 <span class="text-red-500">*</span></h3>
</div>
<div class="p-4">
<template x-for="(item, index) in dates" :key="index">
<div class="flex items-center gap-2 mb-2">
<span class="text-xs text-gray-400 shrink-0" style="width: 24px;" x-text="(index+1) + '.'"></span>
<input type="date" x-model="item.date"
class="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
style="width: 200px;">
<button type="button" @click="removeDate(index)" x-show="dates.length > 1"
class="p-1 text-red-400 hover:text-red-600 transition">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
</button>
</div>
</template>
<button type="button" @click="addDate()"
class="mt-1 text-xs text-blue-600 hover:text-blue-800 font-medium 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="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
</svg>
휴가일 추가
</button>
<p class="text-xs text-gray-400 mt-2">* 잔여연차 일수만큼 지정해주세요.</p>
</div>
</div>
{{-- 4. 통지 내용 (법적 문구) --}}
<div class="border border-red-200 rounded-lg overflow-hidden">
<div class="bg-red-50 px-4 py-2 border-b border-red-200">
<h3 class="text-sm font-semibold text-red-700">4. 법적 통지 문구</h3>
</div>
<div class="p-4">
<div class="text-xs text-gray-600 leading-relaxed space-y-2 bg-gray-50 p-3 rounded border border-gray-200">
<p>귀하는 연차 사용촉진 1 통보 이후에도 연차 사용 시기를 제출하지 않아 근로기준법 제61조에 따라 회사가 다음과 같이 휴가 사용일을 지정합니다.</p>
<p> 지정된 날짜에 연차휴가를 사용하여 주시기 바랍니다.</p>
<p class="text-red-600 font-medium"> 통지서는 근로기준법 제61조에 따른 연차 사용촉진 절차에 의한 통보입니다.</p>
</div>
</div>
</div>
{{-- 미리보기 버튼 --}}
<div class="flex justify-end">
<button type="button" onclick="openLp2Preview()"
class="px-3 py-2 bg-indigo-50 text-indigo-600 hover:bg-indigo-100 border border-indigo-200 rounded-lg text-sm font-medium transition inline-flex items-center gap-1">
<svg class="w-4 h-4" 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>
미리보기
</button>
</div>
</div>
</div>
{{-- 2 통지서 미리보기 모달 --}}
<div id="lp2-preview-modal" style="display: none;" class="fixed inset-0 z-50">
<div class="absolute inset-0 bg-black/50" onclick="closeLp2Preview()"></div>
<div class="relative flex items-center justify-center min-h-full p-4">
<div class="bg-white rounded-xl shadow-2xl w-full overflow-hidden relative" style="max-width: 780px;">
<div class="flex items-center justify-between px-5 py-3 border-b border-gray-200 bg-gray-50">
<h3 class="text-base font-semibold text-gray-800">2 통지서 미리보기</h3>
<div class="flex items-center gap-2">
<button type="button" onclick="printLp2Preview()"
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="closeLp2Preview()"
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="lp2-preview-content" style="padding: 40px 48px; font-family: 'Pretendard', 'Malgun Gothic', sans-serif;"></div>
</div>
</div>
</div>
</div>
@push('scripts')
<script>
function loadLp2EmployeeInfo(userId) {
const sel = document.getElementById('lp2-user-id');
const opt = sel.options[sel.selectedIndex];
document.getElementById('lp2-department').value = opt?.dataset?.department || '';
document.getElementById('lp2-position').value = opt?.dataset?.position || '';
}
function getLp2Dates() {
const container = document.getElementById('leave-promotion-2nd-form-container');
const alpine = container._x_dataStack?.[0];
if (!alpine) return [];
return alpine.dates.filter(d => d.date).map(d => d.date);
}
function buildLp2PreviewHtml(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((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>';
}
function getLp2Data() {
const sel = document.getElementById('lp2-user-id');
const opt = sel.options[sel.selectedIndex];
return {
employee_name: opt?.text?.replace(/\s*\(.*\)/, '') || '',
employee_id: sel.value,
department: document.getElementById('lp2-department').value,
position: document.getElementById('lp2-position').value,
remaining_days: parseFloat(document.getElementById('lp2-remaining-days').value) || 0,
designated_dates: getLp2Dates(),
company_name: document.getElementById('lp2-company-name').value,
ceo_name: document.getElementById('lp2-ceo-name').value,
};
}
function openLp2Preview() {
document.getElementById('lp2-preview-content').innerHTML = buildLp2PreviewHtml(getLp2Data());
document.getElementById('lp2-preview-modal').style.display = '';
}
function closeLp2Preview() { document.getElementById('lp2-preview-modal').style.display = 'none'; }
function printLp2Preview() {
const content = document.getElementById('lp2-preview-content').innerHTML;
const win = window.open('', '_blank');
win.document.write('<html><head><title>연차사용촉진 2차 통지서</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

View File

@@ -0,0 +1,176 @@
{{--
연차유급휴가 사용촉진 통지서 (2) 읽기전용
Props:
$content (array) - approvals.content JSON
--}}
<div class="space-y-4">
{{-- 미리보기 버튼 --}}
<div class="flex justify-end gap-2">
<button type="button" onclick="openLp2ShowPreview()"
class="px-3 py-1.5 bg-indigo-50 text-indigo-600 hover:bg-indigo-100 border border-indigo-200 rounded-lg text-sm font-medium transition inline-flex items-center gap-1">
<svg class="w-4 h-4" 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>
통지서 미리보기
</button>
</div>
{{-- 수신자 정보 --}}
<div class="border border-gray-200 rounded-lg overflow-hidden">
<div class="bg-gray-50 px-4 py-2 border-b border-gray-200">
<h3 class="text-sm font-semibold text-gray-700">수신자 정보</h3>
</div>
<div class="p-4">
<div class="grid gap-3" style="grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));">
<div>
<span class="text-xs text-gray-500">직원명</span>
<div class="text-sm font-medium mt-0.5">{{ $content['employee_name'] ?? '-' }}</div>
</div>
<div>
<span class="text-xs text-gray-500">부서</span>
<div class="text-sm font-medium mt-0.5">{{ $content['department'] ?? '-' }}</div>
</div>
<div>
<span class="text-xs text-gray-500">직급</span>
<div class="text-sm font-medium mt-0.5">{{ $content['position'] ?? '-' }}</div>
</div>
</div>
</div>
</div>
{{-- 잔여 연차 --}}
<div class="border border-amber-200 rounded-lg overflow-hidden">
<div class="bg-amber-50 px-4 py-2 border-b border-amber-200">
<h3 class="text-sm font-semibold text-amber-700">잔여 연차</h3>
</div>
<div class="p-4">
<div class="text-lg font-bold text-amber-800">{{ $content['remaining_days'] ?? 0 }}</div>
</div>
</div>
{{-- 회사 지정 휴가일 --}}
@if(!empty($content['designated_dates']))
<div class="border border-red-200 rounded-lg overflow-hidden">
<div class="bg-red-50 px-4 py-2 border-b border-red-200">
<h3 class="text-sm font-semibold text-red-700">회사 지정 휴가일</h3>
</div>
<div class="p-4">
<div class="space-y-1">
@foreach($content['designated_dates'] as $i => $date)
<div class="flex items-center gap-2 text-sm">
<span class="text-xs text-gray-400 shrink-0" style="width: 24px;">{{ $i + 1 }}.</span>
<span class="font-medium text-gray-800">{{ $date }}</span>
</div>
@endforeach
</div>
</div>
</div>
@endif
{{-- 법적 문구 --}}
<div class="border border-red-200 rounded-lg overflow-hidden">
<div class="bg-red-50 px-4 py-2 border-b border-red-200">
<h3 class="text-sm font-semibold text-red-700">법적 통지 문구</h3>
</div>
<div class="p-4 text-xs text-gray-600 leading-relaxed space-y-1">
<p>귀하는 1 통보 이후에도 연차 사용 시기를 제출하지 않아 회사가 휴가 사용일을 지정합니다.</p>
<p class="text-red-600 font-medium"> 통지서는 근로기준법 제61조에 따른 연차 사용촉진 절차에 의한 통보입니다.</p>
</div>
</div>
</div>
{{-- 미리보기 모달 --}}
<div id="lp2-show-preview-modal" style="display: none;" class="fixed inset-0 z-50">
<div class="absolute inset-0 bg-black/50" onclick="closeLp2ShowPreview()"></div>
<div class="relative flex items-center justify-center min-h-full p-4">
<div class="bg-white rounded-xl shadow-2xl w-full overflow-hidden relative" style="max-width: 780px;">
<div class="flex items-center justify-between px-5 py-3 border-b border-gray-200 bg-gray-50">
<h3 class="text-base font-semibold text-gray-800">2 통지서 미리보기</h3>
<div class="flex items-center gap-2">
<button type="button" onclick="printLp2ShowPreview()"
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="closeLp2ShowPreview()"
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="lp2-show-preview-content" style="padding: 40px 48px; font-family: 'Pretendard', 'Malgun Gothic', sans-serif;"></div>
</div>
</div>
</div>
</div>
@push('scripts')
<script>
function buildLp2ShowPreviewHtml(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>';
}
function openLp2ShowPreview() {
const data = @json($content);
document.getElementById('lp2-show-preview-content').innerHTML = buildLp2ShowPreviewHtml(data);
document.getElementById('lp2-show-preview-modal').style.display = '';
}
function closeLp2ShowPreview() { document.getElementById('lp2-show-preview-modal').style.display = 'none'; }
function printLp2ShowPreview() {
const content = document.getElementById('lp2-show-preview-content').innerHTML;
const win = window.open('', '_blank');
win.document.write('<html><head><title>연차사용촉진 2차 통지서</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

View File

@@ -123,6 +123,10 @@ class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg transition
@include('approvals.partials._quotation-show', ['content' => $approval->content])
@elseif(!empty($approval->content) && $approval->form?->code === 'official_letter')
@include('approvals.partials._official-letter-show', ['content' => $approval->content])
@elseif(!empty($approval->content) && $approval->form?->code === 'leave_promotion_1st')
@include('approvals.partials._leave-promotion-1st-show', ['content' => $approval->content])
@elseif(!empty($approval->content) && $approval->form?->code === 'leave_promotion_2nd')
@include('approvals.partials._leave-promotion-2nd-show', ['content' => $approval->content])
@elseif($approval->body && preg_match('/<[a-z][\s\S]*>/i', $approval->body))
<div class="prose prose-sm max-w-none text-gray-700">
{!! strip_tags($approval->body, '<p><br><strong><b><em><i><u><s><del><h1><h2><h3><h4><h5><h6><ul><ol><li><blockquote><pre><code><a><span><div><table><thead><tbody><tr><th><td>') !!}