feat: [approval] 기안함 휴가신청 → 휴가관리 연동
- 기안함에서 휴가/근태신청/사유서 양식 선택 시 전용 입력 폼 표시 - 양식코드별 유형 필터링 (leave/attendance_request/reason_report) - saveApproval()에서 content에 구조화된 데이터 포함 - handleApprovalCompleted()에서 Leave 없을 시 자동 생성 - createLeaveFromApproval() 메서드 추가
This commit is contained in:
@@ -90,6 +90,11 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-
|
||||
<div id="quill-container" style="display: none; min-height: 300px;"></div>
|
||||
</div>
|
||||
|
||||
{{-- 휴가/근태신청/사유서 전용 폼 --}}
|
||||
@include('approvals.partials._leave-form', [
|
||||
'employees' => $employees ?? collect(),
|
||||
])
|
||||
|
||||
{{-- 지출결의서 전용 폼 --}}
|
||||
@include('approvals.partials._expense-form', [
|
||||
'initialData' => [],
|
||||
@@ -211,6 +216,84 @@ class="p-1 text-gray-400 hover:text-gray-600 transition">
|
||||
const formCodes = @json($forms->pluck('code', 'id'));
|
||||
const linesData = @json($lines);
|
||||
let isExpenseForm = false;
|
||||
let isLeaveForm = 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 '';
|
||||
@@ -375,17 +458,39 @@ function applyQuickLine(lineId) {
|
||||
function switchFormMode(formId) {
|
||||
const code = formCodes[formId];
|
||||
const expenseContainer = document.getElementById('expense-form-container');
|
||||
const leaveContainer = document.getElementById('leave-form-container');
|
||||
const bodyArea = document.getElementById('body-area');
|
||||
const expenseLoadBtn = document.getElementById('expense-load-btn');
|
||||
|
||||
const leaveFormCodes = ['leave', 'attendance_request', 'reason_report'];
|
||||
|
||||
if (code === 'expense') {
|
||||
isExpenseForm = true;
|
||||
isLeaveForm = false;
|
||||
expenseContainer.style.display = '';
|
||||
leaveContainer.style.display = 'none';
|
||||
expenseLoadBtn.style.display = '';
|
||||
bodyArea.style.display = 'none';
|
||||
} else if (leaveFormCodes.includes(code)) {
|
||||
isExpenseForm = false;
|
||||
isLeaveForm = true;
|
||||
expenseContainer.style.display = 'none';
|
||||
leaveContainer.style.display = '';
|
||||
expenseLoadBtn.style.display = 'none';
|
||||
bodyArea.style.display = 'none';
|
||||
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 {
|
||||
isExpenseForm = false;
|
||||
isLeaveForm = false;
|
||||
expenseContainer.style.display = 'none';
|
||||
leaveContainer.style.display = 'none';
|
||||
expenseLoadBtn.style.display = 'none';
|
||||
bodyArea.style.display = '';
|
||||
}
|
||||
@@ -396,7 +501,7 @@ function applyBodyTemplate(formId) {
|
||||
switchFormMode(formId);
|
||||
|
||||
// 전용 폼이면 제목만 자동 설정하고 body template 적용 건너뜀
|
||||
if (isExpenseForm) {
|
||||
if (isExpenseForm || isLeaveForm) {
|
||||
const titleEl = document.getElementById('title');
|
||||
if (!titleEl.value.trim()) {
|
||||
const formSelect = document.getElementById('form_id');
|
||||
@@ -474,21 +579,42 @@ function applyBodyTemplate(formId) {
|
||||
return;
|
||||
}
|
||||
|
||||
let expenseContent = {};
|
||||
let formContent = {};
|
||||
let formBody = getBodyContent();
|
||||
let attachmentFileIds = [];
|
||||
|
||||
if (isExpenseForm) {
|
||||
const expenseEl = document.getElementById('expense-form-container');
|
||||
if (expenseEl && expenseEl._x_dataStack) {
|
||||
expenseContent = expenseEl._x_dataStack[0].getFormData();
|
||||
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);
|
||||
}
|
||||
|
||||
const payload = {
|
||||
form_id: document.getElementById('form_id').value,
|
||||
title: title,
|
||||
body: isExpenseForm ? null : getBodyContent(),
|
||||
content: isExpenseForm ? expenseContent : {},
|
||||
body: formBody,
|
||||
content: formContent,
|
||||
attachment_file_ids: attachmentFileIds,
|
||||
is_urgent: document.getElementById('is_urgent').checked,
|
||||
steps: steps,
|
||||
|
||||
57
resources/views/approvals/partials/_leave-form.blade.php
Normal file
57
resources/views/approvals/partials/_leave-form.blade.php
Normal file
@@ -0,0 +1,57 @@
|
||||
{{--
|
||||
휴가/근태신청/사유서 전용 폼
|
||||
Props:
|
||||
$employees (Collection) - 활성 사원 목록
|
||||
--}}
|
||||
@php
|
||||
$employees = $employees ?? collect();
|
||||
$currentUserId = auth()->id();
|
||||
@endphp
|
||||
|
||||
<div id="leave-form-container" style="display: none;" class="mb-4">
|
||||
<div class="space-y-4">
|
||||
{{-- 신청자 --}}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">신청자 <span class="text-red-500">*</span></label>
|
||||
<select id="leave-user-id"
|
||||
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="max-width: 300px;">
|
||||
@foreach($employees as $emp)
|
||||
<option value="{{ $emp->user_id }}" {{ $emp->user_id == $currentUserId ? 'selected' : '' }}>
|
||||
{{ $emp->display_name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{{-- 유형 --}}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">유형 <span class="text-red-500">*</span></label>
|
||||
<select id="leave-type"
|
||||
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="max-width: 300px;">
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{{-- 시작일 / 종료일 --}}
|
||||
<div class="flex gap-4 flex-wrap">
|
||||
<div style="flex: 1 1 200px; max-width: 300px;">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">시작일 <span class="text-red-500">*</span></label>
|
||||
<input type="date" id="leave-start-date"
|
||||
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 style="flex: 1 1 200px; max-width: 300px;">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">종료일 <span class="text-red-500">*</span></label>
|
||||
<input type="date" id="leave-end-date"
|
||||
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>
|
||||
|
||||
{{-- 사유 --}}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">사유</label>
|
||||
<textarea id="leave-reason" rows="3" 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"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user