Files
sam-manage/resources/views/approvals/create.blade.php
김보곤 b05daffedb fix: [approvals] 재직증명서 양식에 대표자명/회사주소 누락 수정
- 재직증명서 폼/제출/미리보기/PDF에 ceo_name, company_address 추가
- tenants 테이블에서 가져온 회사 정보를 모든 기안 양식에 통일 적용
- 경력/위촉/사직서는 이미 정상 처리, 재직증명서만 누락되어 있었음
2026-03-06 09:19:57 +09:00

1555 lines
69 KiB
PHP

@extends('layouts.app')
@section('title', '기안 작성')
@section('content')
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-6">
<h1 class="text-2xl font-bold text-gray-800">기안 작성</h1>
<a href="{{ route('approvals.drafts') }}" class="text-gray-600 hover:text-gray-800 text-sm">
&larr; 기안함으로 돌아가기
</a>
</div>
<div>
<div class="bg-white rounded-lg shadow-sm p-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4">문서 내용</h2>
{{-- 양식 선택 --}}
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">양식 <span class="text-red-500">*</span></label>
<div class="flex items-center gap-2">
<select id="form_id" class="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
@foreach($forms as $form)
<option value="{{ $form->id }}">{{ $form->name }}</option>
@endforeach
</select>
<button type="button" id="expense-load-btn" style="display: none;" onclick="openExpenseLoadModal()"
class="shrink-0 px-3 py-2 bg-amber-50 text-amber-700 hover:bg-amber-100 border border-amber-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="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/>
</svg>
불러오기
</button>
</div>
</div>
{{-- 제목 --}}
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">제목 <span class="text-red-500">*</span></label>
<input type="text" id="title" maxlength="200" placeholder="결재 제목을 입력하세요"
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>
{{-- 결재선 --}}
<div class="mb-4">
<div class="flex items-center justify-between mb-1">
<label class="block text-sm font-medium text-gray-700">결재선</label>
<div class="flex items-center gap-2">
<select id="quick-line-select" onchange="applyQuickLine(this.value)"
class="px-2 py-1.5 border border-gray-300 rounded-lg text-xs focus:outline-none focus:ring-2 focus:ring-blue-500"
style="min-width: 160px;">
<option value="">결재선 선택</option>
@foreach($lines as $line)
<option value="{{ $line->id }}" {{ ($defaultLine?->id ?? '') == $line->id ? 'selected' : '' }}>
{{ $line->name }} ({{ count($line->steps ?? []) }}단계)
</option>
@endforeach
</select>
<button type="button" onclick="openApprovalLineModal()"
class="px-3 py-1.5 bg-blue-50 text-blue-600 hover:bg-blue-100 rounded-lg text-xs font-medium transition whitespace-nowrap">
세부 설정
</button>
</div>
</div>
<div id="approval-line-summary" class="p-3 bg-gray-50 rounded-lg border border-gray-200 flex items-center">
<span class="text-sm text-gray-400">결재선이 설정되지 않았습니다.</span>
</div>
</div>
{{-- 긴급 여부 --}}
<div class="mb-4">
<label class="inline-flex items-center gap-2 cursor-pointer">
<input type="checkbox" id="is_urgent" class="rounded border-gray-300 text-red-600 focus:ring-red-500">
<span class="text-sm text-gray-700">긴급</span>
</label>
</div>
{{-- 본문 (일반 양식) --}}
<div id="body-area" class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">
본문
<label class="inline-flex items-center gap-1 ml-3 cursor-pointer">
<input type="checkbox" id="useEditor" onchange="toggleEditor()"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span class="text-xs text-gray-500 font-normal">편집기</span>
</label>
</label>
<textarea id="body" rows="12" placeholder="기안 내용을 입력하세요..."
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"
style="min-height: 300px;"></textarea>
<div id="quill-container" style="display: none; min-height: 300px;"></div>
</div>
{{-- 휴가/근태신청/사유서 전용 --}}
@include('approvals.partials._leave-form', [
'employees' => $employees ?? collect(),
])
{{-- 재직증명서 전용 --}}
@include('approvals.partials._certificate-form', [
'employees' => $employees ?? collect(),
])
{{-- 경력증명서 전용 --}}
@include('approvals.partials._career-cert-form', [
'employees' => $employees ?? collect(),
])
{{-- 위촉증명서 전용 --}}
@include('approvals.partials._appointment-cert-form', [
'employees' => $employees ?? collect(),
])
{{-- 사직서 전용 --}}
@include('approvals.partials._resignation-form', [
'employees' => $employees ?? collect(),
])
{{-- 지출결의서 전용 --}}
@include('approvals.partials._expense-form', [
'initialData' => [],
'cards' => $cards ?? collect(),
'accounts' => $accounts ?? collect(),
])
{{-- 액션 버튼 --}}
<div class="border-t pt-4 mt-4 flex gap-2 justify-end">
<button onclick="saveApproval('draft')"
class="bg-gray-600 hover:bg-gray-700 text-white px-6 py-2 rounded-lg transition text-sm font-medium">
임시저장
</button>
<button onclick="saveApproval('submit')"
class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg transition text-sm font-medium">
상신
</button>
</div>
</div>
</div>
{{-- 결재선 모달 --}}
<div id="approval-line-modal" style="display: none;" class="fixed inset-0 z-50">
<div class="absolute inset-0 bg-black/50" onclick="closeApprovalLineModal()"></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: 720px;">
<button type="button" onclick="closeApprovalLineModal()"
class="absolute top-3 right-3 z-10 p-1 text-gray-400 hover:text-gray-600 transition bg-white rounded-full shadow-sm">
<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 class="overflow-y-auto" style="max-height: 70vh;">
@php
$defaultLine = collect($lines)->firstWhere('is_default', true);
@endphp
@include('approvals.partials._approval-line-editor', [
'lines' => $lines,
'initialSteps' => $defaultLine?->steps ?? [],
'selectedLineId' => $defaultLine?->id ?? '',
])
</div>
<div class="px-5 py-3 border-t border-gray-200 flex justify-end">
<button type="button" onclick="closeApprovalLineModal()"
class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium transition">
확인
</button>
</div>
</div>
</div>
</div>
{{-- 지출결의서 불러오기 모달 --}}
<div id="expense-load-modal" style="display: none;" class="fixed inset-0 z-50">
<div class="absolute inset-0 bg-black/50" onclick="closeExpenseLoadModal()"></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: 640px;">
<div class="px-5 py-4 border-b border-gray-200 flex items-center justify-between">
<h3 class="text-lg font-semibold text-gray-800">지출결의서 불러오기</h3>
<button type="button" onclick="closeExpenseLoadModal()"
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 class="overflow-y-auto" style="max-height: 60vh;">
<div id="expense-load-list" class="p-4">
<div class="text-center text-sm text-gray-400 py-8">불러오는 ...</div>
</div>
</div>
</div>
</div>
</div>
@endsection
@push('styles')
<link href="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.snow.css" rel="stylesheet">
<style>
#quill-container .ql-editor { min-height: 260px; font-size: 0.875rem; }
#quill-container .ql-toolbar { border-radius: 0.5rem 0.5rem 0 0; border-color: #d1d5db; }
#quill-container .ql-container { border-radius: 0 0 0.5rem 0.5rem; border-color: #d1d5db; }
#summary-sortable .step-card {
cursor: grab;
position: relative;
margin-right: 24px;
user-select: none;
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
#summary-sortable .step-card:last-child { margin-right: 0; }
#summary-sortable .step-card:not(:last-child)::after {
content: '→';
position: absolute;
right: -18px;
top: 50%;
transform: translateY(-50%);
color: #d1d5db;
font-size: 14px;
pointer-events: none;
}
#summary-sortable .step-card:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
transform: translateY(-1px);
}
#summary-sortable .step-card:active { cursor: grabbing; }
#summary-sortable .step-card.sortable-ghost { opacity: 0.3; }
#summary-sortable .step-card.sortable-chosen {
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
transform: translateY(-2px);
}
</style>
@endpush
@push('scripts')
<script src="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.js"></script>
<script>
let quillInstance = null;
var summarySortableInstance = null;
const formBodyTemplates = @json($forms->pluck('body_template', 'id'));
const formCodes = @json($forms->pluck('code', 'id'));
const linesData = @json($lines);
let isExpenseForm = false;
let isLeaveForm = false;
let isCertForm = false;
let isCareerCertForm = false;
let isAppointmentCertForm = false;
let isResignationForm = false;
// 양식코드별 표시할 유형 목록
const leaveTypesByFormCode = {
leave: [
{ value: 'annual', label: '연차' },
{ value: 'half_am', label: '오전반차' },
{ value: 'half_pm', label: '오후반차' },
{ value: 'sick', label: '병가' },
{ value: 'family', label: '경조사' },
{ value: 'maternity', label: '출산' },
{ value: 'parental', label: '육아' },
],
attendance_request: [
{ value: 'business_trip', label: '출장' },
{ value: 'remote', label: '재택근무' },
{ value: 'field_work', label: '외근' },
{ value: 'early_leave', label: '조퇴' },
],
reason_report: [
{ value: 'late_reason', label: '지각사유서' },
{ value: 'absent_reason', label: '결근사유서' },
],
};
function populateLeaveTypes(formCode) {
const select = document.getElementById('leave-type');
select.innerHTML = '';
const types = leaveTypesByFormCode[formCode] || [];
types.forEach(t => {
const opt = document.createElement('option');
opt.value = t.value;
opt.textContent = t.label;
select.appendChild(opt);
});
}
function getLeaveFormData() {
return {
user_id: parseInt(document.getElementById('leave-user-id').value),
leave_type: document.getElementById('leave-type').value,
start_date: document.getElementById('leave-start-date').value,
end_date: document.getElementById('leave-end-date').value,
reason: document.getElementById('leave-reason').value.trim() || null,
};
}
function buildLeaveBody(data) {
const typeSelect = document.getElementById('leave-type');
const userSelect = document.getElementById('leave-user-id');
const typeName = typeSelect.options[typeSelect.selectedIndex]?.text || data.leave_type;
const userName = userSelect.options[userSelect.selectedIndex]?.text || '';
const rows = [
['신청자', userName],
['유형', typeName],
];
if (data.start_date === data.end_date) {
rows.push(['대상일', data.start_date]);
} else {
rows.push(['기간', data.start_date + ' ~ ' + data.end_date]);
}
if (data.reason) {
rows.push(['사유', data.reason]);
}
let html = '<p>아래와 같이 신청합니다.</p>';
html += '<table style="border-collapse:collapse; width:100%; margin-top:12px; font-size:14px;">';
rows.forEach(([label, value]) => {
html += '<tr><th style="padding:8px 12px; background:#f8f9fa; border:1px solid #dee2e6; text-align:left; width:120px; font-weight:600;">'
+ escapeHtml(label) + '</th><td style="padding:8px 12px; border:1px solid #dee2e6;">'
+ escapeHtml(value) + '</td></tr>';
});
html += '</table>';
return html;
}
function escapeHtml(str) {
if (!str) return '';
const div = document.createElement('div');
div.appendChild(document.createTextNode(str));
return div.innerHTML;
}
function toggleEditor() {
const useEditor = document.getElementById('useEditor').checked;
const textarea = document.getElementById('body');
const container = document.getElementById('quill-container');
if (useEditor) {
container.style.display = '';
textarea.style.display = 'none';
if (!quillInstance) {
quillInstance = new Quill('#quill-container', {
theme: 'snow',
modules: {
toolbar: [
[{ 'header': [1, 2, 3, false] }],
['bold', 'italic', 'underline', 'strike'],
[{ 'color': [] }, { 'background': [] }],
[{ 'list': 'ordered' }, { 'list': 'bullet' }],
[{ 'align': [] }],
['blockquote', 'code-block'],
['link'],
['clean'],
],
},
placeholder: '기안 내용을 입력하세요...',
});
}
const text = textarea.value.trim();
if (text) {
if (/<[a-z][\s\S]*>/i.test(text)) {
quillInstance.root.innerHTML = text;
} else {
quillInstance.setText(text);
}
}
} else {
container.style.display = 'none';
textarea.style.display = '';
if (quillInstance) {
const html = quillInstance.root.innerHTML;
textarea.value = (html === '<p><br></p>') ? '' : html;
}
}
}
function getBodyContent() {
const useEditor = document.getElementById('useEditor')?.checked;
if (useEditor && quillInstance) {
const html = quillInstance.root.innerHTML;
return (html === '<p><br></p>') ? '' : html;
}
return document.getElementById('body').value;
}
function openApprovalLineModal() {
document.getElementById('approval-line-modal').style.display = '';
document.body.style.overflow = 'hidden';
}
function closeApprovalLineModal() {
document.getElementById('approval-line-modal').style.display = 'none';
document.body.style.overflow = '';
updateApprovalLineSummary();
// 모달 내부 결재선 선택과 외부 드롭다운 동기화
const editorEl = document.getElementById('approval-line-editor');
if (editorEl && editorEl._x_dataStack) {
document.getElementById('quick-line-select').value = editorEl._x_dataStack[0].selectedLineId || '';
}
}
function updateApprovalLineSummary() {
const editorEl = document.getElementById('approval-line-editor');
if (!editorEl || !editorEl._x_dataStack) return;
const steps = editorEl._x_dataStack[0].steps;
const summaryEl = document.getElementById('approval-line-summary');
if (summarySortableInstance) { summarySortableInstance.destroy(); summarySortableInstance = null; }
if (!steps || steps.length === 0) {
summaryEl.className = 'p-3 bg-gray-50 rounded-lg border border-gray-200 flex items-center';
summaryEl.innerHTML = '<span class="text-sm text-gray-400">결재선이 설정되지 않았습니다.</span>';
return;
}
const typeCounters = { approval: 0, agreement: 0, reference: 0 };
const typeLabels = { approval: '결재', agreement: '합의', reference: '참조' };
const typeBg = { approval: 'bg-blue-50 border-blue-100', agreement: 'bg-green-50 border-green-100', reference: 'bg-gray-100 border-gray-200' };
const typeLabelColor = { approval: 'text-blue-600', agreement: 'text-green-600', reference: 'text-gray-500' };
const cards = [];
steps.forEach(function(s, i) {
typeCounters[s.step_type] = (typeCounters[s.step_type] || 0) + 1;
var count = typeCounters[s.step_type];
var label = typeLabels[s.step_type] || s.step_type;
var bg = typeBg[s.step_type] || typeBg.reference;
var labelColor = typeLabelColor[s.step_type] || typeLabelColor.reference;
var stepLabel = s.step_type === 'reference' ? label : count + '차 ' + label;
var position = s.position || '';
cards.push(
'<div class="step-card text-center px-3 py-2 rounded-lg border ' + bg + '" data-index="' + i + '" style="min-width: 72px;">' +
'<div class="text-xs font-medium ' + labelColor + '">' + escapeHtml(stepLabel) + '</div>' +
(position ? '<div class="text-xs text-gray-400 mt-0.5">' + escapeHtml(position) + '</div>' : '') +
'<div class="text-sm font-semibold text-gray-800 mt-0.5">' + escapeHtml(s.user_name) + '</div>' +
'</div>'
);
});
summaryEl.className = 'p-3 bg-gray-50 rounded-lg border border-gray-200';
summaryEl.innerHTML = '<div id="summary-sortable" class="flex flex-wrap items-center">' + cards.join('') + '</div>' +
(steps.length > 1 ? '<div class="text-xs text-gray-400 mt-2">드래그하여 순서를 변경할 수 있습니다</div>' : '');
initSummarySortable();
}
function initSummarySortable() {
if (summarySortableInstance) { summarySortableInstance.destroy(); summarySortableInstance = null; }
var el = document.getElementById('summary-sortable');
if (!el || typeof Sortable === 'undefined') return;
summarySortableInstance = Sortable.create(el, {
animation: 150,
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
onEnd: function(evt) {
if (evt.oldIndex === evt.newIndex) return;
var editorEl = document.getElementById('approval-line-editor');
if (!editorEl || !editorEl._x_dataStack) return;
var steps = editorEl._x_dataStack[0].steps;
var item = steps.splice(evt.oldIndex, 1)[0];
steps.splice(evt.newIndex, 0, item);
updateApprovalLineSummary();
}
});
}
function applyQuickLine(lineId) {
const editorEl = document.getElementById('approval-line-editor');
if (!editorEl || !editorEl._x_dataStack) return;
const alpineData = editorEl._x_dataStack[0];
if (!lineId) {
alpineData.selectedLineId = '';
alpineData.steps = [];
} else {
alpineData.selectedLineId = lineId;
alpineData.loadLine();
}
setTimeout(updateApprovalLineSummary, 100);
}
function switchFormMode(formId) {
const code = formCodes[formId];
const expenseContainer = document.getElementById('expense-form-container');
const leaveContainer = document.getElementById('leave-form-container');
const certContainer = document.getElementById('cert-form-container');
const careerCertContainer = document.getElementById('career-cert-form-container');
const appointmentCertContainer = document.getElementById('appointment-cert-form-container');
const resignationContainer = document.getElementById('resignation-form-container');
const bodyArea = document.getElementById('body-area');
const expenseLoadBtn = document.getElementById('expense-load-btn');
const leaveFormCodes = ['leave', 'attendance_request', 'reason_report'];
// 모두 숨기기
expenseContainer.style.display = 'none';
leaveContainer.style.display = 'none';
certContainer.style.display = 'none';
careerCertContainer.style.display = 'none';
appointmentCertContainer.style.display = 'none';
resignationContainer.style.display = 'none';
expenseLoadBtn.style.display = 'none';
bodyArea.style.display = 'none';
isExpenseForm = false;
isLeaveForm = false;
isCertForm = false;
isCareerCertForm = false;
isAppointmentCertForm = false;
isResignationForm = false;
if (code === 'expense') {
isExpenseForm = true;
expenseContainer.style.display = '';
expenseLoadBtn.style.display = '';
} else if (leaveFormCodes.includes(code)) {
isLeaveForm = true;
leaveContainer.style.display = '';
populateLeaveTypes(code);
const today = new Date().toISOString().slice(0, 10);
const startEl = document.getElementById('leave-start-date');
const endEl = document.getElementById('leave-end-date');
if (!startEl.value) startEl.value = today;
if (!endEl.value) endEl.value = today;
} else if (code === 'employment_cert') {
isCertForm = true;
certContainer.style.display = '';
const certUserId = document.getElementById('cert-user-id').value;
if (certUserId) loadCertInfo(certUserId);
} else if (code === 'career_cert') {
isCareerCertForm = true;
careerCertContainer.style.display = '';
const ccUserId = document.getElementById('cc-user-id').value;
if (ccUserId) loadCareerCertInfo(ccUserId);
} else if (code === 'appointment_cert') {
isAppointmentCertForm = true;
appointmentCertContainer.style.display = '';
const acUserId = document.getElementById('ac-user-id').value;
if (acUserId) loadAppointmentCertInfo(acUserId);
} else if (code === 'resignation') {
isResignationForm = true;
resignationContainer.style.display = '';
const rgUserId = document.getElementById('rg-user-id').value;
if (rgUserId) loadResignationInfo(rgUserId);
} else {
bodyArea.style.display = '';
}
}
function applyBodyTemplate(formId) {
// 먼저 폼 모드 전환
switchFormMode(formId);
// 전용 폼이면 제목을 양식명으로 설정하고 body template 적용 건너뜀
if (isExpenseForm || isLeaveForm || isCertForm || isCareerCertForm || isAppointmentCertForm || isResignationForm) {
const titleEl = document.getElementById('title');
const formSelect = document.getElementById('form_id');
titleEl.value = formSelect.options[formSelect.selectedIndex].text;
return;
}
const template = formBodyTemplates[formId];
if (!template) return;
const bodyContent = getBodyContent();
if (bodyContent.trim()) {
if (!confirm('현재 본문 내용을 양식 템플릿으로 교체하시겠습니까?')) return;
}
// 제목 자동 설정 (비어있을 때만)
const titleEl = document.getElementById('title');
if (!titleEl.value.trim()) {
const formSelect = document.getElementById('form_id');
titleEl.value = formSelect.options[formSelect.selectedIndex].text;
}
// 편집기 활성화 후 HTML 삽입
const useEditorEl = document.getElementById('useEditor');
if (!useEditorEl.checked) {
useEditorEl.checked = true;
toggleEditor();
}
if (quillInstance) {
quillInstance.root.innerHTML = template;
}
document.getElementById('body').value = template;
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
const expModal = document.getElementById('expense-load-modal');
if (expModal && expModal.style.display !== 'none') {
closeExpenseLoadModal();
return;
}
const modal = document.getElementById('approval-line-modal');
if (modal && modal.style.display !== 'none') {
closeApprovalLineModal();
}
}
});
document.addEventListener('DOMContentLoaded', function() {
setTimeout(updateApprovalLineSummary, 200);
// 초기 양식에 대한 폼 모드 전환
switchFormMode(document.getElementById('form_id').value);
// 양식 변경 시 본문 템플릿 자동 채움
document.getElementById('form_id').addEventListener('change', function() {
applyBodyTemplate(this.value);
});
});
async function saveApproval(action) {
const title = document.getElementById('title').value.trim();
if (!title) {
showToast('제목을 입력해주세요.', 'warning');
return;
}
const editorEl = document.getElementById('approval-line-editor');
const steps = editorEl._x_dataStack[0].getStepsData();
if (action === 'submit' && steps.filter(s => s.step_type !== 'reference').length === 0) {
showToast('결재자를 1명 이상 추가해주세요.', 'warning');
return;
}
let formContent = {};
let formBody = getBodyContent();
let attachmentFileIds = [];
if (isExpenseForm) {
const expenseEl = document.getElementById('expense-form-container');
if (expenseEl && expenseEl._x_dataStack) {
formContent = expenseEl._x_dataStack[0].getFormData();
attachmentFileIds = expenseEl._x_dataStack[0].getFileIds();
}
formBody = null;
} else if (isLeaveForm) {
const leaveData = getLeaveFormData();
if (!leaveData.leave_type) {
showToast('유형을 선택해주세요.', 'warning');
return;
}
if (!leaveData.start_date || !leaveData.end_date) {
showToast('시작일과 종료일을 입력해주세요.', 'warning');
return;
}
if (leaveData.start_date > leaveData.end_date) {
showToast('종료일이 시작일보다 이전입니다.', 'warning');
return;
}
formContent = leaveData;
formBody = buildLeaveBody(leaveData);
} else if (isCertForm) {
const purpose = getCertPurpose();
if (!purpose) {
showToast('사용용도를 입력해주세요.', 'warning');
return;
}
formContent = {
cert_user_id: document.getElementById('cert-user-id').value,
name: document.getElementById('cert-name').value,
resident_number: document.getElementById('cert-resident').value,
address: document.getElementById('cert-address').value,
department: document.getElementById('cert-department').value,
position: document.getElementById('cert-position').value,
hire_date: document.getElementById('cert-hire-date').value,
company_name: document.getElementById('cert-company').value,
business_num: document.getElementById('cert-business-num').value,
ceo_name: document.getElementById('cert-ceo-name').value,
company_address: document.getElementById('cert-company-address').value,
purpose: purpose,
issue_date: document.getElementById('cert-issue-date').value,
};
formBody = null;
} else if (isCareerCertForm) {
const purpose = getCareerCertPurpose();
if (!purpose) {
showToast('사용용도를 입력해주세요.', 'warning');
return;
}
formContent = {
cert_user_id: document.getElementById('cc-user-id').value,
name: document.getElementById('cc-name').value,
birth_date: document.getElementById('cc-birth-date').value,
address: document.getElementById('cc-address').value,
department: document.getElementById('cc-department').value,
position: document.getElementById('cc-position').value,
hire_date: document.getElementById('cc-hire-date').value,
resign_date: document.getElementById('cc-resign-date').value,
job_description: document.getElementById('cc-job-description').value,
company_name: document.getElementById('cc-company').value,
business_num: document.getElementById('cc-business-num').value,
ceo_name: document.getElementById('cc-ceo-name').value,
phone: document.getElementById('cc-phone').value,
company_address: document.getElementById('cc-company-address').value,
purpose: purpose,
issue_date: document.getElementById('cc-issue-date').value,
};
formBody = null;
} else if (isAppointmentCertForm) {
const purpose = getAppointmentCertPurpose();
if (!purpose) {
showToast('용도를 입력해주세요.', 'warning');
return;
}
formContent = {
cert_user_id: document.getElementById('ac-user-id').value,
name: document.getElementById('ac-name').value,
resident_number: document.getElementById('ac-resident').value,
department: document.getElementById('ac-department').value,
phone: document.getElementById('ac-phone').value,
hire_date: document.getElementById('ac-hire-date').value,
resign_date: document.getElementById('ac-resign-date').value,
contract_type: document.getElementById('ac-contract-type').value,
company_name: document.getElementById('ac-company-name').value,
ceo_name: document.getElementById('ac-ceo-name').value,
purpose: purpose,
issue_date: document.getElementById('ac-issue-date').value,
};
formBody = null;
} else if (isResignationForm) {
const reason = getResignationReason();
if (!reason) {
showToast('사직사유를 입력해주세요.', 'warning');
return;
}
const resignDate = document.getElementById('rg-resign-date').value;
if (!resignDate) {
showToast('퇴사(예정)일을 입력해주세요.', 'warning');
return;
}
formContent = {
cert_user_id: document.getElementById('rg-user-id').value,
name: document.getElementById('rg-name').value,
resident_number: document.getElementById('rg-resident').value,
department: document.getElementById('rg-department').value,
position: document.getElementById('rg-position').value,
hire_date: document.getElementById('rg-hire-date').value,
resign_date: resignDate,
address: document.getElementById('rg-address').value,
reason: reason,
company_name: document.getElementById('rg-company-name').value,
ceo_name: document.getElementById('rg-ceo-name').value,
issue_date: document.getElementById('rg-issue-date').value,
};
formBody = null;
}
const payload = {
form_id: document.getElementById('form_id').value,
title: title,
body: formBody,
content: formContent,
attachment_file_ids: attachmentFileIds,
is_urgent: document.getElementById('is_urgent').checked,
steps: steps,
};
try {
const response = await fetch('/api/admin/approvals', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
},
body: JSON.stringify(payload),
});
const data = await response.json();
if (!data.success && !data.data) {
showToast(data.message || '저장에 실패했습니다.', 'error');
return;
}
if (action === 'submit') {
const submitResponse = await fetch(`/api/admin/approvals/${data.data.id}/submit`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
},
});
const submitData = await submitResponse.json();
if (submitData.success) {
showToast('결재가 상신되었습니다.', 'success');
setTimeout(() => location.href = '/approval-mgmt/drafts', 500);
} else {
showToast(submitData.message || '상신에 실패했습니다.', 'error');
}
} else {
showToast('임시저장되었습니다.', 'success');
setTimeout(() => location.href = `/approval-mgmt/${data.data.id}/edit`, 500);
}
} catch (e) {
showToast('서버 오류가 발생했습니다.', 'error');
}
}
// =========================================================================
// 지출결의서 불러오기
// =========================================================================
const expenseTypeLabels = {
corporate_card: '법인카드', transfer: '송금', auto_transfer: '자동이체 출금',
cash_advance: '현금/가지급정산',
};
const statusColors = {
draft: 'bg-gray-100 text-gray-600', pending: 'bg-blue-100 text-blue-600',
approved: 'bg-green-100 text-green-600', rejected: 'bg-red-100 text-red-600',
cancelled: 'bg-yellow-100 text-yellow-600',
};
async function openExpenseLoadModal() {
const modal = document.getElementById('expense-load-modal');
const list = document.getElementById('expense-load-list');
modal.style.display = '';
document.body.style.overflow = 'hidden';
list.innerHTML = '<div class="text-center text-sm text-gray-400 py-8">불러오는 중...</div>';
try {
const res = await fetch('/api/admin/approvals/expense-history', {
headers: { 'Accept': 'application/json' },
});
const json = await res.json();
if (!json.success || !json.data.length) {
list.innerHTML = '<div class="text-center text-sm text-gray-400 py-8">이전 지출결의서가 없습니다.</div>';
return;
}
list.innerHTML = json.data.map(item => {
const amount = parseInt(item.total_amount || 0).toLocaleString('ko-KR');
const typeLabel = expenseTypeLabels[item.expense_type] || item.expense_type;
const color = statusColors[item.status] || statusColors.draft;
return `<div class="flex items-center justify-between p-3 border border-gray-200 rounded-lg hover:bg-blue-50 cursor-pointer transition mb-2" onclick="loadExpenseData(${item.id})">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<span class="text-sm font-medium text-gray-800 truncate">${escapeHtml(item.title)}</span>
<span class="shrink-0 px-1.5 py-0.5 rounded text-xs font-medium ${color}">${escapeHtml(item.status_label)}</span>
</div>
<div class="flex items-center gap-3 mt-1 text-xs text-gray-500">
<span>${escapeHtml(item.created_at)}</span>
<span>${escapeHtml(typeLabel)}</span>
<span class="font-medium text-gray-700">${amount}원</span>
</div>
</div>
<svg class="w-4 h-4 text-gray-400 shrink-0 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</div>`;
}).join('');
} catch (e) {
list.innerHTML = '<div class="text-center text-sm text-red-400 py-8">불러오기 실패</div>';
}
}
function closeExpenseLoadModal() {
document.getElementById('expense-load-modal').style.display = 'none';
document.body.style.overflow = '';
}
async function loadExpenseData(approvalId) {
try {
const res = await fetch(`/api/admin/approvals/${approvalId}`, {
headers: { 'Accept': 'application/json' },
});
const json = await res.json();
if (!json.success || !json.data?.content) {
showToast('데이터를 불러올 수 없습니다.', 'error');
return;
}
const content = json.data.content;
const expenseEl = document.getElementById('expense-form-container');
if (!expenseEl || !expenseEl._x_dataStack) return;
const alpine = expenseEl._x_dataStack[0];
const today = new Date().toISOString().slice(0, 10);
// 폼 데이터 복사 (날짜는 오늘로 초기화)
alpine.formData.expense_type = content.expense_type || 'corporate_card';
alpine.formData.tax_invoice = content.tax_invoice || 'normal';
alpine.formData.write_date = today;
alpine.formData.approval_date = today;
alpine.formData.department = content.department || '경리부';
alpine.formData.writer_name = content.writer_name || '';
alpine.formData.attachment_memo = content.attachment_memo || '';
alpine.formData.selected_card = content.selected_card || null;
alpine.formData.selected_account = content.selected_account || null;
// 내역 항목 복사 (날짜는 오늘로)
if (content.items && content.items.length > 0) {
let keyCounter = alpine.formData.items.length + 100;
alpine.formData.items = content.items.map(item => ({
_key: ++keyCounter,
date: today,
description: item.description || '',
amount: parseInt(item.amount) || 0,
vendor: item.vendor || '',
bank: item.bank || '',
account_no: item.account_no || '',
depositor: item.depositor || '',
remark: item.remark || '',
}));
}
// 제목 설정
const titleEl = document.getElementById('title');
if (!titleEl.value.trim()) {
titleEl.value = json.data.title || '지출결의서';
}
closeExpenseLoadModal();
showToast('지출결의서를 불러왔습니다. 내용을 확인 후 수정해주세요.', 'success');
} catch (e) {
showToast('불러오기 실패', 'error');
}
}
// =========================================================================
// 재직증명서 관련 함수
// =========================================================================
async function loadCertInfo(userId) {
if (!userId) return;
try {
const resp = await fetch(`/api/admin/approvals/cert-info/${userId}`, {
headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' }
});
const data = await resp.json();
if (data.success) {
const d = data.data;
document.getElementById('cert-name').value = d.name || '';
document.getElementById('cert-resident').value = d.resident_number || '';
document.getElementById('cert-address').value = d.address || '';
document.getElementById('cert-company').value = d.company_name || '';
document.getElementById('cert-business-num').value = d.business_num || '';
document.getElementById('cert-department').value = d.department || '';
document.getElementById('cert-position').value = d.position || '';
document.getElementById('cert-hire-date').value = d.hire_date ? d.hire_date + ' ~' : '';
document.getElementById('cert-ceo-name').value = d.ceo_name || '';
document.getElementById('cert-company-address').value = d.company_address || '';
} else {
showToast(data.message || '사원 정보를 불러올 수 없습니다.', 'error');
}
} catch (e) {
showToast('사원 정보 조회 중 오류가 발생했습니다.', 'error');
}
}
function onCertPurposeChange() {
const sel = document.getElementById('cert-purpose-select');
const customWrap = document.getElementById('cert-purpose-custom-wrap');
if (sel.value === '__custom__') {
customWrap.style.display = '';
document.getElementById('cert-purpose-custom').focus();
} else {
customWrap.style.display = 'none';
}
}
function getCertPurpose() {
const sel = document.getElementById('cert-purpose-select');
if (sel.value === '__custom__') {
return document.getElementById('cert-purpose-custom').value.trim();
}
return sel.value;
}
function openCertPreview() {
const name = document.getElementById('cert-name').value || '-';
const resident = document.getElementById('cert-resident').value || '-';
const address = document.getElementById('cert-address').value || '-';
const company = document.getElementById('cert-company').value || '-';
const businessNum = document.getElementById('cert-business-num').value || '-';
const department = document.getElementById('cert-department').value || '-';
const position = document.getElementById('cert-position').value || '-';
const hireDate = document.getElementById('cert-hire-date').value || '-';
const purpose = getCertPurpose() || '-';
const issueDate = document.getElementById('cert-issue-date').value || '-';
const ceoName = document.getElementById('cert-ceo-name').value || '-';
const issueDateFormatted = issueDate !== '-' ? issueDate.replace(/-/g, function(m, i) { return i === 4 ? '년 ' : '월 '; }) + '일' : '-';
const el = document.getElementById('cert-preview-content');
el.innerHTML = buildCertPreviewHtml({ name, resident, address, company, businessNum, department, position, hireDate, purpose, issueDateFormatted, ceoName });
document.getElementById('cert-preview-modal').style.display = '';
document.body.style.overflow = 'hidden';
}
function closeCertPreview() {
document.getElementById('cert-preview-modal').style.display = 'none';
document.body.style.overflow = '';
}
function printCertPreview() {
const content = document.getElementById('cert-preview-content').innerHTML;
const win = window.open('', '_blank', 'width=800,height=1000');
win.document.write('<html><head><title>재직증명서</title>');
win.document.write('<style>body{font-family:"Pretendard","Malgun Gothic",sans-serif;padding:48px 56px;margin:0;} table{border-collapse:collapse;width:100%;} th,td{border:1px solid #333;padding:10px 14px;font-size:14px;} th{background:#f8f9fa;font-weight:600;text-align:left;white-space:nowrap;width:120px;} @media print{body{padding:40px 48px;}}</style>');
win.document.write('</head><body>');
win.document.write(content);
win.document.write('</body></html>');
win.document.close();
win.onload = function() { win.print(); };
}
// =========================================================================
// 경력증명서 관련 함수
// =========================================================================
async function loadCareerCertInfo(userId) {
if (!userId) return;
try {
const resp = await fetch(`/api/admin/approvals/career-cert-info/${userId}`, {
headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' }
});
const data = await resp.json();
if (data.success) {
const d = data.data;
document.getElementById('cc-name').value = d.name || '';
document.getElementById('cc-birth-date').value = d.birth_date || '';
document.getElementById('cc-address').value = d.address || '';
document.getElementById('cc-company').value = d.company_name || '';
document.getElementById('cc-business-num').value = d.business_num || '';
document.getElementById('cc-ceo-name').value = d.ceo_name || '';
document.getElementById('cc-phone').value = d.phone || '';
document.getElementById('cc-company-address').value = d.company_address || '';
document.getElementById('cc-department').value = d.department || '';
document.getElementById('cc-position').value = d.position || '';
document.getElementById('cc-hire-date').value = d.hire_date || '';
document.getElementById('cc-resign-date').value = d.resign_date || '';
document.getElementById('cc-job-description').value = d.job_description || '';
} else {
showToast(data.message || '사원 정보를 불러올 수 없습니다.', 'error');
}
} catch (e) {
showToast('사원 정보 조회 중 오류가 발생했습니다.', 'error');
}
}
function onCareerCertPurposeChange() {
const sel = document.getElementById('cc-purpose-select');
const customWrap = document.getElementById('cc-purpose-custom-wrap');
if (sel.value === '__custom__') {
customWrap.style.display = '';
document.getElementById('cc-purpose-custom').focus();
} else {
customWrap.style.display = 'none';
}
}
function getCareerCertPurpose() {
const sel = document.getElementById('cc-purpose-select');
if (sel.value === '__custom__') {
return document.getElementById('cc-purpose-custom').value.trim();
}
return sel.value;
}
function openCareerCertPreview() {
const name = document.getElementById('cc-name').value || '-';
const birthDate = document.getElementById('cc-birth-date').value || '-';
const address = document.getElementById('cc-address').value || '-';
const company = document.getElementById('cc-company').value || '-';
const businessNum = document.getElementById('cc-business-num').value || '-';
const ceoName = document.getElementById('cc-ceo-name').value || '-';
const phone = document.getElementById('cc-phone').value || '-';
const companyAddress = document.getElementById('cc-company-address').value || '-';
const department = document.getElementById('cc-department').value || '-';
const position = document.getElementById('cc-position').value || '-';
const hireDate = document.getElementById('cc-hire-date').value || '-';
const resignDate = document.getElementById('cc-resign-date').value || '현재';
const jobDescription = document.getElementById('cc-job-description').value || '-';
const purpose = getCareerCertPurpose() || '-';
const issueDate = document.getElementById('cc-issue-date').value || '-';
const issueDateFormatted = issueDate !== '-' ? issueDate.replace(/-/g, function(m, i) { return i === 4 ? '년 ' : '월 '; }) + '일' : '-';
const el = document.getElementById('career-cert-preview-content');
el.innerHTML = buildCareerCertPreviewHtml({ name, birthDate, address, company, businessNum, ceoName, phone, companyAddress, department, position, hireDate, resignDate, jobDescription, purpose, issueDateFormatted });
document.getElementById('career-cert-preview-modal').style.display = '';
document.body.style.overflow = 'hidden';
}
function closeCareerCertPreview() {
document.getElementById('career-cert-preview-modal').style.display = 'none';
document.body.style.overflow = '';
}
function printCareerCertPreview() {
const content = document.getElementById('career-cert-preview-content').innerHTML;
const win = window.open('', '_blank', 'width=800,height=1000');
win.document.write('<html><head><title>경력증명서</title>');
win.document.write('<style>body{font-family:"Pretendard","Malgun Gothic",sans-serif;padding:48px 56px;margin:0;} table{border-collapse:collapse;width:100%;} th,td{border:1px solid #333;padding:10px 14px;font-size:14px;} th{background:#f8f9fa;font-weight:600;text-align:left;white-space:nowrap;width:120px;} @media print{body{padding:40px 48px;}}</style>');
win.document.write('</head><body>');
win.document.write(content);
win.document.write('</body></html>');
win.document.close();
win.onload = function() { win.print(); };
}
function buildCareerCertPreviewHtml(d) {
const e = (s) => { const div = document.createElement('div'); div.textContent = s; return div.innerHTML; };
const thStyle = 'border:1px solid #333; padding:10px 14px; background:#f8f9fa; font-weight:600; text-align:left; white-space:nowrap; width:120px; font-size:14px;';
const tdStyle = 'border:1px solid #333; padding:10px 14px; font-size:14px;';
return `
<h1 style="text-align:center; font-size:28px; font-weight:700; letter-spacing:12px; margin-bottom:40px;">경 력 증 명 서</h1>
<h3 style="font-size:15px; font-weight:600; margin:24px 0 10px; color:#333;">1. 인적사항</h3>
<table style="border-collapse:collapse; width:100%; margin-bottom:20px;">
<tr>
<th style="${thStyle}">성 명</th>
<td style="${tdStyle}">${e(d.name)}</td>
<th style="${thStyle}">생년월일</th>
<td style="${tdStyle}">${e(d.birthDate)}</td>
</tr>
<tr>
<th style="${thStyle}">주 소</th>
<td style="${tdStyle}" colspan="3">${e(d.address)}</td>
</tr>
</table>
<h3 style="font-size:15px; font-weight:600; margin:24px 0 10px; color:#333;">2. 경력사항</h3>
<table style="border-collapse:collapse; width:100%; margin-bottom:20px;">
<tr>
<th style="${thStyle}">회 사 명</th>
<td style="${tdStyle}">${e(d.company)}</td>
<th style="${thStyle}">사업자번호</th>
<td style="${tdStyle}">${e(d.businessNum)}</td>
</tr>
<tr>
<th style="${thStyle}">대 표 자</th>
<td style="${tdStyle}">${e(d.ceoName)}</td>
<th style="${thStyle}">대표전화</th>
<td style="${tdStyle}">${e(d.phone)}</td>
</tr>
<tr>
<th style="${thStyle}">소 재 지</th>
<td style="${tdStyle}" colspan="3">${e(d.companyAddress)}</td>
</tr>
<tr>
<th style="${thStyle}">소속부서</th>
<td style="${tdStyle}">${e(d.department)}</td>
<th style="${thStyle}">직위/직급</th>
<td style="${tdStyle}">${e(d.position)}</td>
</tr>
<tr>
<th style="${thStyle}">근무기간</th>
<td style="${tdStyle}" colspan="3">${e(d.hireDate)} ~ ${e(d.resignDate)}</td>
</tr>
<tr>
<th style="${thStyle}">담당업무</th>
<td style="${tdStyle}" colspan="3">${e(d.jobDescription)}</td>
</tr>
</table>
<h3 style="font-size:15px; font-weight:600; margin:24px 0 10px; color:#333;">3. 발급정보</h3>
<table style="border-collapse:collapse; width:100%; margin-bottom:32px;">
<tr>
<th style="${thStyle}">사용용도</th>
<td style="${tdStyle}" colspan="3">${e(d.purpose)}</td>
</tr>
</table>
<p style="text-align:center; font-size:15px; line-height:2; margin:36px 0;">
위 사람은 당사에 재직(근무) 하였음을 증명합니다.
</p>
<p style="text-align:center; font-size:15px; font-weight:500; margin-bottom:48px;">
${e(d.issueDateFormatted)}
</p>
<div style="text-align:center; margin-top:32px;">
<p style="font-size:16px; font-weight:600; margin-bottom:8px;">${e(d.company)}</p>
<p style="font-size:14px; color:#555;">대표이사 &nbsp;&nbsp; ${e(d.ceoName)} &nbsp;&nbsp; (인)</p>
</div>
`;
}
// =========================================================================
// 위촉증명서 관련 함수
// =========================================================================
async function loadAppointmentCertInfo(userId) {
if (!userId) return;
try {
const resp = await fetch(`/api/admin/approvals/appointment-cert-info/${userId}`, {
headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' }
});
const data = await resp.json();
if (data.success) {
const d = data.data;
document.getElementById('ac-name').value = d.name || '';
document.getElementById('ac-resident').value = d.resident_number || '';
document.getElementById('ac-department').value = d.department || '';
document.getElementById('ac-phone').value = d.phone || '';
document.getElementById('ac-hire-date').value = d.hire_date || '';
document.getElementById('ac-resign-date').value = d.resign_date || '';
document.getElementById('ac-contract-type').value = d.contract_type || '';
document.getElementById('ac-company-name').value = d.company_name || '';
document.getElementById('ac-ceo-name').value = d.ceo_name || '';
} else {
showToast(data.message || '사원 정보를 불러올 수 없습니다.', 'error');
}
} catch (e) {
showToast('사원 정보 조회 중 오류가 발생했습니다.', 'error');
}
}
function onAppointmentCertPurposeChange() {
const sel = document.getElementById('ac-purpose-select');
const customWrap = document.getElementById('ac-purpose-custom-wrap');
if (sel.value === '__custom__') {
customWrap.style.display = '';
document.getElementById('ac-purpose-custom').focus();
} else {
customWrap.style.display = 'none';
}
}
function getAppointmentCertPurpose() {
const sel = document.getElementById('ac-purpose-select');
if (sel.value === '__custom__') {
return document.getElementById('ac-purpose-custom').value.trim();
}
return sel.value;
}
function openAppointmentCertPreview() {
const name = document.getElementById('ac-name').value || '-';
const resident = document.getElementById('ac-resident').value || '-';
const department = document.getElementById('ac-department').value || '-';
const phone = document.getElementById('ac-phone').value || '-';
const hireDate = document.getElementById('ac-hire-date').value || '-';
const resignDate = document.getElementById('ac-resign-date').value || '현재';
const contractType = document.getElementById('ac-contract-type').value || '-';
const purpose = getAppointmentCertPurpose() || '-';
const issueDate = document.getElementById('ac-issue-date').value || '-';
const issueDateFormatted = issueDate !== '-' ? issueDate.replace(/-/g, function(m, i) { return i === 4 ? '년 ' : '월 '; }) + '일' : '-';
const company = document.getElementById('ac-company-name').value || '-';
const ceoName = document.getElementById('ac-ceo-name').value || '-';
const el = document.getElementById('appointment-cert-preview-content');
el.innerHTML = buildAppointmentCertPreviewHtml({ name, resident, department, phone, hireDate, resignDate, contractType, purpose, issueDateFormatted, company, ceoName });
document.getElementById('appointment-cert-preview-modal').style.display = '';
document.body.style.overflow = 'hidden';
}
function closeAppointmentCertPreview() {
document.getElementById('appointment-cert-preview-modal').style.display = 'none';
document.body.style.overflow = '';
}
function printAppointmentCertPreview() {
const content = document.getElementById('appointment-cert-preview-content').innerHTML;
const win = window.open('', '_blank', 'width=800,height=1000');
win.document.write('<html><head><title>위촉증명서</title>');
win.document.write('<style>body{font-family:"Pretendard","Malgun Gothic",sans-serif;padding:48px 56px;margin:0;} table{border-collapse:collapse;width:100%;} th,td{border:1px solid #333;padding:10px 14px;font-size:14px;} th{background:#f8f9fa;font-weight:600;text-align:left;white-space:nowrap;width:120px;} @media print{body{padding:40px 48px;}}</style>');
win.document.write('</head><body>');
win.document.write(content);
win.document.write('</body></html>');
win.document.close();
win.onload = function() { win.print(); };
}
function buildAppointmentCertPreviewHtml(d) {
const e = (s) => { const div = document.createElement('div'); div.textContent = s; return div.innerHTML; };
const thStyle = 'border:1px solid #333; padding:10px 14px; background:#f8f9fa; font-weight:600; text-align:left; white-space:nowrap; width:120px; font-size:14px;';
const tdStyle = 'border:1px solid #333; padding:10px 14px; font-size:14px;';
return `
<h1 style="text-align:center; font-size:28px; font-weight:700; letter-spacing:12px; margin-bottom:40px;">위 촉 증 명 서</h1>
<table style="border-collapse:collapse; width:100%; margin-bottom:20px;">
<tr>
<th style="${thStyle}">성 명</th>
<td style="${tdStyle}" colspan="3">${e(d.name)}</td>
</tr>
<tr>
<th style="${thStyle}">주민등록번호</th>
<td style="${tdStyle}" colspan="3">${e(d.resident)}</td>
</tr>
<tr>
<th style="${thStyle}">소 속</th>
<td style="${tdStyle}">${e(d.department)}</td>
<th style="${thStyle}">연 락 처</th>
<td style="${tdStyle}">${e(d.phone)}</td>
</tr>
<tr>
<th style="${thStyle}">위촉(재직)기간</th>
<td style="${tdStyle}">${e(d.hireDate)} ~ ${e(d.resignDate)}</td>
<th style="${thStyle}">계약자격</th>
<td style="${tdStyle}">${e(d.contractType)}</td>
</tr>
<tr>
<th style="${thStyle}">용 도</th>
<td style="${tdStyle}" colspan="3">${e(d.purpose)}</td>
</tr>
</table>
<p style="text-align:center; font-size:15px; line-height:2; margin:36px 0;">
위와 같이 위촉하였음을 증명합니다.
</p>
<p style="text-align:center; font-size:15px; font-weight:500; margin-bottom:48px;">
${e(d.issueDateFormatted)}
</p>
<div style="text-align:center; margin-top:32px;">
<p style="font-size:16px; font-weight:600; margin-bottom:8px;">${e(d.company)}</p>
<p style="font-size:14px; color:#555;">대표이사 &nbsp;&nbsp; ${e(d.ceoName)} &nbsp;&nbsp; (인)</p>
</div>
`;
}
// =========================================================================
// 사직서 관련 함수
// =========================================================================
async function loadResignationInfo(userId) {
if (!userId) return;
try {
const resp = await fetch(`/api/admin/approvals/resignation-info/${userId}`, {
headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' }
});
const data = await resp.json();
if (data.success) {
const d = data.data;
document.getElementById('rg-name').value = d.name || '';
document.getElementById('rg-resident').value = d.resident_number || '';
document.getElementById('rg-department').value = d.department || '';
document.getElementById('rg-position').value = d.position || '';
document.getElementById('rg-hire-date').value = d.hire_date || '';
document.getElementById('rg-address').value = d.address || '';
document.getElementById('rg-company-name').value = d.company_name || '';
document.getElementById('rg-ceo-name').value = d.ceo_name || '';
} else {
showToast(data.message || '사원 정보를 불러올 수 없습니다.', 'error');
}
} catch (e) {
showToast('사원 정보 조회 중 오류가 발생했습니다.', 'error');
}
}
function onResignationReasonChange() {
const sel = document.getElementById('rg-reason-select');
const customWrap = document.getElementById('rg-reason-custom-wrap');
if (sel.value === '__custom__') {
customWrap.style.display = '';
document.getElementById('rg-reason-custom').focus();
} else {
customWrap.style.display = 'none';
}
}
function getResignationReason() {
const sel = document.getElementById('rg-reason-select');
if (sel.value === '__custom__') {
return document.getElementById('rg-reason-custom').value.trim();
}
return sel.value;
}
function openResignationPreview() {
const department = document.getElementById('rg-department').value || '-';
const position = document.getElementById('rg-position').value || '-';
const name = document.getElementById('rg-name').value || '-';
const resident = document.getElementById('rg-resident').value || '-';
const hireDate = document.getElementById('rg-hire-date').value || '-';
const resignDate = document.getElementById('rg-resign-date').value || '-';
const address = document.getElementById('rg-address').value || '-';
const reason = getResignationReason() || '-';
const issueDate = document.getElementById('rg-issue-date').value || '-';
const issueDateFormatted = issueDate !== '-' ? issueDate.replace(/-/g, function(m, i) { return i === 4 ? '년 ' : '월 '; }) + '일' : '-';
const company = document.getElementById('rg-company-name').value || '-';
const ceoName = document.getElementById('rg-ceo-name').value || '-';
const el = document.getElementById('resignation-preview-content');
el.innerHTML = buildResignationPreviewHtml({ department, position, name, resident, hireDate, resignDate, address, reason, issueDateFormatted, company, ceoName });
document.getElementById('resignation-preview-modal').style.display = '';
document.body.style.overflow = 'hidden';
}
function closeResignationPreview() {
document.getElementById('resignation-preview-modal').style.display = 'none';
document.body.style.overflow = '';
}
function printResignationPreview() {
const content = document.getElementById('resignation-preview-content').innerHTML;
const win = window.open('', '_blank', 'width=800,height=1000');
win.document.write('<html><head><title>사직서</title>');
win.document.write('<style>body{font-family:"Pretendard","Malgun Gothic",sans-serif;padding:48px 56px;margin:0;} table{border-collapse:collapse;width:100%;} th,td{border:1px solid #333;padding:10px 14px;font-size:14px;} th{background:#f8f9fa;font-weight:600;text-align:left;white-space:nowrap;width:120px;} @media print{body{padding:40px 48px;}}</style>');
win.document.write('</head><body>');
win.document.write(content);
win.document.write('</body></html>');
win.document.close();
win.onload = function() { win.print(); };
}
function buildResignationPreviewHtml(d) {
const e = (s) => { const div = document.createElement('div'); div.textContent = s; return div.innerHTML; };
const thStyle = 'border:1px solid #333; padding:10px 14px; background:#f8f9fa; font-weight:600; text-align:left; white-space:nowrap; width:120px; font-size:14px;';
const tdStyle = 'border:1px solid #333; padding:10px 14px; font-size:14px;';
return `
<h1 style="text-align:center; font-size:28px; font-weight:700; letter-spacing:12px; margin-bottom:40px;">사 직 서</h1>
<table style="border-collapse:collapse; width:100%; margin-bottom:20px;">
<tr>
<th style="${thStyle}">소 속</th>
<td style="${tdStyle}">${e(d.department)}</td>
<th style="${thStyle}">직 위</th>
<td style="${tdStyle}">${e(d.position)}</td>
</tr>
<tr>
<th style="${thStyle}">성 명</th>
<td style="${tdStyle}">${e(d.name)}</td>
<th style="${thStyle}">주민등록번호</th>
<td style="${tdStyle}">${e(d.resident)}</td>
</tr>
<tr>
<th style="${thStyle}">입사일</th>
<td style="${tdStyle}">${e(d.hireDate)}</td>
<th style="${thStyle}">퇴사(예정)일</th>
<td style="${tdStyle}">${e(d.resignDate)}</td>
</tr>
<tr>
<th style="${thStyle}">주 소</th>
<td style="${tdStyle}" colspan="3">${e(d.address)}</td>
</tr>
<tr>
<th style="${thStyle}">사 유</th>
<td style="${tdStyle}" colspan="3">${e(d.reason)}</td>
</tr>
</table>
<p style="text-align:center; font-size:15px; line-height:1.8; margin:36px 0;">
상기 본인은 위 사유로 인하여 사직하고자<br>
이에 사직서를 제출하오니 허가하여 주시기 바랍니다.
</p>
<p style="text-align:center; font-size:15px; font-weight:500; margin-bottom:24px;">
${e(d.issueDateFormatted)}
</p>
<p style="text-align:center; font-size:14px; margin-bottom:48px;">
신청인 &nbsp;&nbsp; ${e(d.name)} &nbsp;&nbsp; (인)
</p>
<div style="text-align:center; margin-top:32px;">
<p style="font-size:16px; font-weight:600;">${e(d.company)} &nbsp;&nbsp; 대표이사 귀하</p>
</div>
`;
}
function buildCertPreviewHtml(d) {
const e = (s) => { const div = document.createElement('div'); div.textContent = s; return div.innerHTML; };
const thStyle = 'border:1px solid #333; padding:10px 14px; background:#f8f9fa; font-weight:600; text-align:left; white-space:nowrap; width:120px; font-size:14px;';
const tdStyle = 'border:1px solid #333; padding:10px 14px; font-size:14px;';
return `
<h1 style="text-align:center; font-size:28px; font-weight:700; letter-spacing:12px; margin-bottom:40px;">재 직 증 명 서</h1>
<h3 style="font-size:15px; font-weight:600; margin:24px 0 10px; color:#333;">1. 인적사항</h3>
<table style="border-collapse:collapse; width:100%; margin-bottom:20px;">
<tr>
<th style="${thStyle}">성 명</th>
<td style="${tdStyle}">${e(d.name)}</td>
<th style="${thStyle}">주민등록번호</th>
<td style="${tdStyle}">${e(d.resident)}</td>
</tr>
<tr>
<th style="${thStyle}">주 소</th>
<td style="${tdStyle}" colspan="3">${e(d.address)}</td>
</tr>
</table>
<h3 style="font-size:15px; font-weight:600; margin:24px 0 10px; color:#333;">2. 재직사항</h3>
<table style="border-collapse:collapse; width:100%; margin-bottom:20px;">
<tr>
<th style="${thStyle}">회 사 명</th>
<td style="${tdStyle}" colspan="3">${e(d.company)}</td>
</tr>
<tr>
<th style="${thStyle}">사업자번호</th>
<td style="${tdStyle}" colspan="3">${e(d.businessNum)}</td>
</tr>
<tr>
<th style="${thStyle}">근무부서</th>
<td style="${tdStyle}">${e(d.department)}</td>
<th style="${thStyle}">직 급</th>
<td style="${tdStyle}">${e(d.position)}</td>
</tr>
<tr>
<th style="${thStyle}">재직기간</th>
<td style="${tdStyle}" colspan="3">${e(d.hireDate)}</td>
</tr>
</table>
<h3 style="font-size:15px; font-weight:600; margin:24px 0 10px; color:#333;">3. 발급정보</h3>
<table style="border-collapse:collapse; width:100%; margin-bottom:32px;">
<tr>
<th style="${thStyle}">사용용도</th>
<td style="${tdStyle}" colspan="3">${e(d.purpose)}</td>
</tr>
</table>
<p style="text-align:center; font-size:15px; line-height:2; margin:36px 0;">
위 사항을 증명합니다.
</p>
<p style="text-align:center; font-size:15px; font-weight:500; margin-bottom:48px;">
${e(d.issueDateFormatted)}
</p>
<div style="text-align:center; margin-top:32px;">
<p style="font-size:16px; font-weight:600; margin-bottom:8px;">${e(d.company)}</p>
<p style="font-size:14px; color:#555;">대표이사 &nbsp;&nbsp; ${e(d.ceoName)} &nbsp;&nbsp; (인)</p>
</div>
`;
}
</script>
@endpush