feat: [approvals] 기안 작성/수정 결재선을 모달로 전환
- 2열 레이아웃(양식 50% + 결재선 50%)을 1열 풀와이드로 변경 - 결재선 편집기를 모달로 이동, 메인 화면에 요약 바만 표시 - ESC 키로 모달 닫기 지원 - edit 페이지 로드 시 기존 결재선 요약 즉시 표시
This commit is contained in:
@@ -19,7 +19,7 @@
|
||||
</div>
|
||||
|
||||
@if($approval->status === 'rejected')
|
||||
<div class="bg-red-50 border-l-4 border-red-400 p-4 mb-6 rounded">
|
||||
<div class="bg-red-50 border-l-4 border-red-400 p-4 mb-6 rounded" style="max-width: 960px; margin-left: auto; margin-right: auto;">
|
||||
<div class="flex items-center">
|
||||
<span class="text-red-700 font-medium">반려됨</span>
|
||||
</div>
|
||||
@@ -35,87 +35,118 @@
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex flex-col lg:flex-row gap-6">
|
||||
{{-- 좌측: 양식 --}}
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="bg-white rounded-lg shadow-sm p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4">문서 내용</h2>
|
||||
<div class="mx-auto" style="max-width: 960px;">
|
||||
<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>
|
||||
<select id="form_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">
|
||||
@foreach($forms as $form)
|
||||
<option value="{{ $form->id }}" {{ $approval->form_id == $form->id ? 'selected' : '' }}>{{ $form->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">양식 <span class="text-red-500">*</span></label>
|
||||
<select id="form_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">
|
||||
@foreach($forms as $form)
|
||||
<option value="{{ $form->id }}" {{ $approval->form_id == $form->id ? 'selected' : '' }}>{{ $form->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</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" value="{{ $approval->title }}"
|
||||
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">
|
||||
<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" value="{{ $approval->title }}"
|
||||
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">
|
||||
<label class="inline-flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" id="is_urgent" {{ $approval->is_urgent ? 'checked' : '' }}
|
||||
class="rounded border-gray-300 text-red-600 focus:ring-red-500">
|
||||
<span class="text-sm text-gray-700">긴급</span>
|
||||
<div class="mb-4">
|
||||
<label class="inline-flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" id="is_urgent" {{ $approval->is_urgent ? 'checked' : '' }}
|
||||
class="rounded border-gray-300 text-red-600 focus:ring-red-500">
|
||||
<span class="text-sm text-gray-700">긴급</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div 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>
|
||||
</div>
|
||||
</label>
|
||||
<textarea id="body" rows="12"
|
||||
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;">{{ $approval->body }}</textarea>
|
||||
<div id="quill-container" style="display: none; min-height: 300px;"></div>
|
||||
</div>
|
||||
|
||||
<div 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"
|
||||
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;">{{ $approval->body }}</textarea>
|
||||
<div id="quill-container" style="display: none; min-height: 300px;"></div>
|
||||
{{-- 결재선 요약 바 --}}
|
||||
<div class="border-t pt-4 mt-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<label class="text-sm font-medium text-gray-700">결재선</label>
|
||||
<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">
|
||||
결재선 설정
|
||||
</button>
|
||||
</div>
|
||||
<div id="approval-line-summary" class="text-sm text-gray-400">
|
||||
결재선이 설정되지 않았습니다.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 우측: 결재선 --}}
|
||||
<div class="shrink-0" style="width: 100%; max-width: 680px;">
|
||||
@php
|
||||
$initialSteps = $approval->steps->map(fn($s) => [
|
||||
'user_id' => $s->approver_id,
|
||||
'user_name' => $s->approver_name ?? ($s->approver?->name ?? ''),
|
||||
'department' => $s->approver_department ?? '',
|
||||
'position' => $s->approver_position ?? '',
|
||||
'step_type' => $s->step_type,
|
||||
])->toArray();
|
||||
@endphp
|
||||
@include('approvals.partials._approval-line-editor', [
|
||||
'lines' => $lines,
|
||||
'initialSteps' => $initialSteps,
|
||||
'selectedLineId' => $approval->line_id ?? '',
|
||||
])
|
||||
|
||||
<div class="mt-4 space-y-2">
|
||||
{{-- 액션 버튼 --}}
|
||||
<div class="border-t pt-4 mt-4 flex gap-2 justify-end">
|
||||
<button onclick="updateApproval('save')"
|
||||
class="w-full bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg transition text-sm font-medium">
|
||||
class="bg-gray-600 hover:bg-gray-700 text-white px-6 py-2 rounded-lg transition text-sm font-medium">
|
||||
저장
|
||||
</button>
|
||||
<button onclick="updateApproval('submit')"
|
||||
class="w-full bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition text-sm font-medium">
|
||||
class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg transition text-sm font-medium">
|
||||
{{ $approval->status === 'rejected' ? '재상신' : '상신' }}
|
||||
</button>
|
||||
@if($approval->isDeletable())
|
||||
<button onclick="deleteApproval()"
|
||||
class="w-full bg-red-100 hover:bg-red-200 text-red-700 px-4 py-2 rounded-lg transition text-sm font-medium">
|
||||
class="bg-red-100 hover:bg-red-200 text-red-700 px-6 py-2 rounded-lg transition text-sm font-medium">
|
||||
삭제
|
||||
</button>
|
||||
@endif
|
||||
</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
|
||||
$initialSteps = $approval->steps->map(fn($s) => [
|
||||
'user_id' => $s->approver_id,
|
||||
'user_name' => $s->approver_name ?? ($s->approver?->name ?? ''),
|
||||
'department' => $s->approver_department ?? '',
|
||||
'position' => $s->approver_position ?? '',
|
||||
'step_type' => $s->step_type,
|
||||
])->toArray();
|
||||
@endphp
|
||||
@include('approvals.partials._approval-line-editor', [
|
||||
'lines' => $lines,
|
||||
'initialSteps' => $initialSteps,
|
||||
'selectedLineId' => $approval->line_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>
|
||||
@endsection
|
||||
|
||||
@push('styles')
|
||||
@@ -185,13 +216,58 @@ function getBodyContent() {
|
||||
return document.getElementById('body').value;
|
||||
}
|
||||
|
||||
// 기존 HTML body 자동 감지 → 편집기 자동 활성화
|
||||
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();
|
||||
}
|
||||
|
||||
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 (!steps || steps.length === 0) {
|
||||
summaryEl.innerHTML = '<span class="text-gray-400">결재선이 설정되지 않았습니다.</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
const typeLabels = { approval: '결재', agreement: '합의', reference: '참조' };
|
||||
const parts = steps.map(s => {
|
||||
const label = typeLabels[s.step_type] || s.step_type;
|
||||
return '<span class="inline-flex items-center gap-1"><strong class="text-gray-800">' + s.user_name + '</strong> <span class="text-gray-400 text-xs">(' + label + ')</span></span>';
|
||||
});
|
||||
|
||||
summaryEl.innerHTML = '<div class="flex flex-wrap items-center gap-1 text-sm">' +
|
||||
parts.join('<span class="text-gray-300 mx-0.5">→</span>') + '</div>';
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
const modal = document.getElementById('approval-line-modal');
|
||||
if (modal && modal.style.display !== 'none') {
|
||||
closeApprovalLineModal();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 기존 HTML body 자동 감지 → 편집기 자동 활성화 + 결재선 요약 초기화
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const existingBody = document.getElementById('body').value;
|
||||
if (/<[a-z][\s\S]*>/i.test(existingBody)) {
|
||||
document.getElementById('useEditor').checked = true;
|
||||
toggleEditor();
|
||||
}
|
||||
|
||||
// Alpine 초기화 후 기존 결재선 요약 표시
|
||||
setTimeout(updateApprovalLineSummary, 200);
|
||||
});
|
||||
|
||||
async function updateApproval(action) {
|
||||
|
||||
Reference in New Issue
Block a user