diff --git a/resources/views/hr/leaves/index.blade.php b/resources/views/hr/leaves/index.blade.php
index 9aa913d3..e6948fc8 100644
--- a/resources/views/hr/leaves/index.blade.php
+++ b/resources/views/hr/leaves/index.blade.php
@@ -232,19 +232,36 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 f
{{-- 결재선 선택 --}}
-
+
-
-
+ @if($approvalLines->isEmpty())
+ {{-- 결재선 없음 경고 + 바로 생성 버튼 --}}
+
+
등록된 결재선이 없습니다. 결재선을 먼저 생성해주세요.
+
+
+
+
+ @else
+
+
+
+ @endif
@@ -304,6 +321,152 @@ class="px-4 py-2 text-sm text-white bg-red-600 hover:bg-red-700 rounded-lg trans
+{{-- 빠른 결재선 생성 모달 --}}
+
+
+
+
+ {{-- 헤더 --}}
+
+
+ {{-- 본문 --}}
+
+ {{-- 결재선 이름 --}}
+
+
+
+
+
+ {{-- 2단 레이아웃: 인원 목록 | 결재선 편집 --}}
+
+ {{-- 왼쪽: 인원 목록 --}}
+
+
+
+
+
+
+
+
+
+ 인원이 없습니다.
+
+
+
+
+
+
+
+ {{-- 오른쪽: 결재선 편집 --}}
+
+
+ 결재선 구성
+ (드래그로 순서 변경)
+
+
+
+ 왼쪽에서 결재자를 추가하세요.
+
+
+
+
+ {{-- 드래그 핸들 --}}
+
+
+
+ {{-- 순번 --}}
+
+ {{-- 이름/부서 --}}
+
+
+
+
+ {{-- 유형 선택 --}}
+
+ {{-- 삭제 --}}
+
+
+
+
+
+
+
+
+
+ {{-- 푸터 --}}
+
+
+ 결재:
+ 합의:
+ 참조:
+
+
+
+
+
+
+
+
+
+
@endsection
@push('scripts')
@@ -601,7 +764,7 @@ function showToast(message, type) {
}
const colors = { success: 'bg-emerald-500', error: 'bg-red-500', info: 'bg-blue-500' };
const toast = document.createElement('div');
- toast.className = `fixed top-4 right-4 z-[60] px-4 py-3 rounded-lg text-white text-sm shadow-lg ${colors[type] || colors.info}`;
+ toast.className = `fixed top-4 right-4 z-[70] px-4 py-3 rounded-lg text-white text-sm shadow-lg ${colors[type] || colors.info}`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
@@ -614,5 +777,259 @@ function showToast(message, type) {
document.getElementById('leaveEndDate').value = this.value;
}
});
+
+ // ===== 빠른 결재선 생성 모달 =====
+ function openQuickLineModal() {
+ document.getElementById('quickLineModal').classList.remove('hidden');
+ }
+
+ function closeQuickLineModal() {
+ document.getElementById('quickLineModal').classList.add('hidden');
+ }
+
+ function quickLineEditor() {
+ let keyCounter = 0;
+
+ return {
+ departments: [],
+ steps: [],
+ lineName: '',
+ searchQuery: '',
+ expandedDepts: {},
+ loading: true,
+ saving: false,
+ sortableInstance: null,
+
+ async init() {
+ try {
+ const res = await fetch('/api/admin/tenant-users/list', {
+ headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' }
+ });
+ const data = await res.json();
+ if (data.success) {
+ this.departments = data.data;
+ this.departments.forEach(d => {
+ this.expandedDepts[d.department_id ?? 'none'] = true;
+ });
+ }
+ } catch (e) {
+ console.error('인원 목록 로딩 실패:', e);
+ }
+ this.loading = false;
+ },
+
+ get filteredDepartments() {
+ if (!this.searchQuery.trim()) return this.departments;
+ const q = this.searchQuery.trim().toLowerCase();
+ return this.departments
+ .map(dept => {
+ const deptMatch = dept.department_name.toLowerCase().includes(q);
+ const matchedUsers = dept.users.filter(u =>
+ u.name.toLowerCase().includes(q) ||
+ (u.position || '').toLowerCase().includes(q) ||
+ (u.job_title || '').toLowerCase().includes(q)
+ );
+ if (deptMatch) return dept;
+ if (matchedUsers.length > 0) return { ...dept, users: matchedUsers };
+ return null;
+ })
+ .filter(Boolean);
+ },
+
+ toggleDept(deptId) {
+ this.expandedDepts[deptId] = !this.expandedDepts[deptId];
+ },
+
+ isDeptExpanded(deptId) {
+ return !!this.expandedDepts[deptId];
+ },
+
+ isAdded(userId) {
+ return this.steps.some(s => s.user_id === userId);
+ },
+
+ addStep(user, deptName) {
+ if (this.isAdded(user.id)) return;
+ this.steps.push({
+ _key: ++keyCounter,
+ user_id: user.id,
+ user_name: user.name,
+ department: deptName || '',
+ position: user.position || user.job_title || '',
+ step_type: 'approval',
+ });
+ this.$nextTick(() => this.initSortable());
+ },
+
+ removeStep(index) {
+ this.steps.splice(index, 1);
+ this.$nextTick(() => this.initSortable());
+ },
+
+ initSortable() {
+ if (this.sortableInstance) {
+ this.sortableInstance.destroy();
+ this.sortableInstance = null;
+ }
+ const el = this.$refs.sortableList;
+ if (!el || typeof Sortable === 'undefined') return;
+
+ this.sortableInstance = Sortable.create(el, {
+ handle: '.drag-handle',
+ animation: 150,
+ ghostClass: 'opacity-30',
+ onEnd: (evt) => {
+ const item = this.steps.splice(evt.oldIndex, 1)[0];
+ this.steps.splice(evt.newIndex, 0, item);
+ },
+ });
+ },
+
+ countByType(type) {
+ return this.steps.filter(s => s.step_type === type).length;
+ },
+
+ async save() {
+ if (!this.lineName.trim()) {
+ showToast('결재선 이름을 입력해주세요.', 'error');
+ return;
+ }
+ const nonRefSteps = this.steps.filter(s => s.step_type !== 'reference');
+ if (nonRefSteps.length === 0) {
+ showToast('결재자를 1명 이상 추가해주세요.', 'error');
+ return;
+ }
+
+ this.saving = true;
+ try {
+ const payload = {
+ name: this.lineName.trim(),
+ steps: this.steps.map(s => ({
+ user_id: s.user_id,
+ step_type: s.step_type,
+ })),
+ is_default: false,
+ };
+
+ const res = await fetch('/api/admin/approvals/lines', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ 'X-CSRF-TOKEN': '{{ csrf_token() }}',
+ },
+ body: JSON.stringify(payload),
+ });
+
+ const data = await res.json();
+ if (res.ok && data.success) {
+ showToast('결재선이 생성되었습니다.', 'success');
+ closeQuickLineModal();
+ refreshApprovalLines(data.data?.id || null);
+ // 상태 초기화
+ this.lineName = '';
+ this.steps = [];
+ } else {
+ const msg = data.message || (data.errors ? Object.values(data.errors).flat().join('\n') : '결재선 생성에 실패했습니다.');
+ showToast(msg, 'error');
+ }
+ } catch (e) {
+ console.error('결재선 생성 실패:', e);
+ showToast('네트워크 오류가 발생했습니다.', 'error');
+ }
+ this.saving = false;
+ },
+ };
+ }
+
+ // ===== 결재선 드롭다운 갱신 =====
+ async function refreshApprovalLines(selectedLineId) {
+ try {
+ const res = await fetch('/api/admin/approvals/lines', {
+ headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' }
+ });
+ const data = await res.json();
+ if (!data.success || !data.data) return;
+
+ const lines = data.data;
+ const section = document.getElementById('approvalLineSection');
+ if (!section) return;
+
+ // 경고 영역 제거 (있으면)
+ const warning = document.getElementById('noLineWarning');
+ if (warning) warning.remove();
+
+ // 기존 hidden input 제거 (결재선 없음 상태에서 만들어진 것)
+ const hiddenInput = section.querySelector('input[type="hidden"][name="approval_line_id"]');
+ if (hiddenInput) hiddenInput.remove();
+
+ // 기존 select가 있으면 재사용, 없으면 새로 생성
+ let select = document.getElementById('leaveApprovalLine');
+ let isNewSelect = false;
+
+ if (!select || select.tagName !== 'SELECT') {
+ // hidden input이었던 경우 제거
+ if (select) select.remove();
+
+ select = document.createElement('select');
+ select.name = 'approval_line_id';
+ select.id = 'leaveApprovalLine';
+ select.required = true;
+ select.setAttribute('onchange', 'previewApprovalSteps()');
+ select.className = 'w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500';
+ isNewSelect = true;
+ }
+
+ // option 목록 재구성
+ select.innerHTML = '';
+ lines.forEach(line => {
+ const opt = document.createElement('option');
+ opt.value = line.id;
+ opt.dataset.steps = JSON.stringify(line.steps || []);
+ opt.textContent = line.name + (line.is_default ? ' (기본)' : '') + ' — ' + (line.steps?.length || 0) + '단계';
+ if (selectedLineId && line.id == selectedLineId) {
+ opt.selected = true;
+ } else if (!selectedLineId && line.is_default) {
+ opt.selected = true;
+ }
+ select.appendChild(opt);
+ });
+
+ if (isNewSelect) {
+ // label 다음에 삽입
+ const label = section.querySelector('label');
+ if (label) {
+ label.after(select);
+ } else {
+ section.prepend(select);
+ }
+ }
+
+ // 미리보기 영역 확보
+ let preview = document.getElementById('approvalStepsPreview');
+ if (!preview) {
+ preview = document.createElement('div');
+ preview.id = 'approvalStepsPreview';
+ preview.className = 'mt-2 flex items-center gap-1 overflow-x-auto py-1';
+ select.after(preview);
+ }
+
+ // "새 결재선 추가" 링크 확보
+ if (!section.querySelector('.quick-line-link')) {
+ const link = document.createElement('button');
+ link.type = 'button';
+ link.className = 'quick-line-link text-xs text-blue-600 hover:underline mt-1';
+ link.textContent = '+ 새 결재선 추가';
+ link.onclick = openQuickLineModal;
+ preview.after(link);
+ }
+
+ // 미리보기 갱신
+ previewApprovalSteps();
+
+ } catch (e) {
+ console.error('결재선 목록 갱신 실패:', e);
+ }
+ }
@endpush