feat: [org-chart] 부서 숨기기 기능 추가
- 부서 헤더 더블클릭 시 숨기기 버튼 표시 - 숨긴 부서와 하위 부서 트리에서 제거, 연결선 자동 조정 - 숨겨진 부서 패널에서 눈 아이콘 클릭으로 복원
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
@section('title', '조직도 관리')
|
||||
|
||||
@section('content')
|
||||
<div x-data="orgChart()" x-init="init()" @click="handleClick($event)">
|
||||
<div x-data="orgChart()" x-init="init()" @click="handleClick($event)" @dblclick="handleDblClick($event)">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-800 flex items-center gap-2">
|
||||
<i class="ri-organization-chart text-purple-600"></i>
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg px-4 py-3 mb-6 text-sm text-blue-700 flex items-start gap-2">
|
||||
<i class="ri-information-line text-lg shrink-0 mt-0.5"></i>
|
||||
<span>직원 카드를 드래그하여 부서에 배치합니다. 부서 헤더를 드래그하여 순서를 변경하거나 다른 부서 아래로 이동할 수 있습니다.</span>
|
||||
<span>직원 카드를 드래그하여 부서에 배치합니다. 부서 헤더를 드래그하여 순서를 변경하거나 다른 부서 아래로 이동할 수 있습니다. 부서 헤더를 <b>더블클릭</b>하면 숨기기 버튼이 나타납니다.</span>
|
||||
</div>
|
||||
|
||||
<!-- 미배치 직원 패널 -->
|
||||
@@ -43,6 +43,28 @@ class="px-3 py-1 border border-gray-200 rounded text-xs focus:outline-none focus
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 숨겨진 부서 패널 -->
|
||||
<div x-show="hiddenDeptList.length > 0" class="mb-4">
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 px-4 py-3">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<i class="ri-eye-off-line text-gray-400"></i>
|
||||
<span class="text-sm font-semibold text-gray-600">숨겨진 부서</span>
|
||||
<span class="bg-gray-100 text-gray-500 text-xs font-bold px-2 py-0.5 rounded-full" x-text="hiddenDeptList.length"></span>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<template x-for="dept in hiddenDeptList" :key="dept.id">
|
||||
<span class="inline-flex items-center gap-1.5 bg-gray-50 border border-gray-200 rounded-lg px-3 py-1.5 text-sm text-gray-700">
|
||||
<span x-text="dept.name"></span>
|
||||
<button data-action="restore-dept" :data-dept-id="dept.id"
|
||||
class="text-purple-500 hover:text-purple-700 transition" title="다시 표시">
|
||||
<i class="ri-eye-line text-sm"></i>
|
||||
</button>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 조직도 트리 -->
|
||||
<div class="overflow-x-auto pb-8">
|
||||
<div class="org-tree" style="display:flex;flex-direction:column;align-items:center;">
|
||||
@@ -105,6 +127,8 @@ function orgChart() {
|
||||
deptSortables: [],
|
||||
empSortables: [],
|
||||
unassignedSortable: null,
|
||||
hiddenDepts: new Set(),
|
||||
dblClickDept: null,
|
||||
|
||||
get totalEmployees() { return this.employees.length; },
|
||||
get assignedCount() { return this.employees.filter(e => e.department_id).length; },
|
||||
@@ -187,7 +211,7 @@ function orgChart() {
|
||||
},
|
||||
|
||||
buildChildrenHtml(parentId, level) {
|
||||
const children = this.getChildrenSorted(parentId);
|
||||
const children = this.getChildrenSorted(parentId).filter(d => !this.isDeptHidden(d.id));
|
||||
if (!children.length && parentId !== null) {
|
||||
return `<div class="org-children org-drop-target" data-parent-id="${parentId}">하위 부서 드롭</div>`;
|
||||
}
|
||||
@@ -212,11 +236,14 @@ function orgChart() {
|
||||
let h = `<div class="org-node-wrap" data-dept-id="${dept.id}">`;
|
||||
h += `<div style="width:1px;height:24px;background:#D1D5DB;"></div>`;
|
||||
h += `<div style="width:${s.w};border:${s.border};border-radius:12px;background:#fff;text-align:center;overflow:hidden;">`;
|
||||
h += `<div class="dept-drag-handle" style="cursor:grab;padding:8px 12px;background:${s.hBg};border-bottom:1px solid ${s.hBdr};display:flex;align-items:center;justify-content:center;gap:4px;">`;
|
||||
h += `<div class="dept-drag-handle" data-action="dept-dblclick" data-dept-id="${dept.id}" style="cursor:grab;padding:8px 12px;background:${s.hBg};border-bottom:1px solid ${s.hBdr};display:flex;align-items:center;justify-content:center;gap:4px;position:relative;">`;
|
||||
h += `<i class="ri-drag-move-2-line" style="color:${s.iconClr};opacity:0.5;font-size:11px;"></i>`;
|
||||
h += `<i class="${s.icon}" style="color:${s.iconClr};font-size:13px;"></i>`;
|
||||
h += `<span style="font-weight:700;font-size:13px;color:#1F2937;">${this.esc(dept.name)}</span>`;
|
||||
if (dept.code) h += `<span style="font-size:11px;color:#9CA3AF;">(${this.esc(dept.code)})</span>`;
|
||||
if (this.dblClickDept === dept.id) {
|
||||
h += `<button data-action="hide-dept" data-dept-id="${dept.id}" style="position:absolute;right:4px;top:50%;transform:translateY(-50%);background:#EF4444;color:#fff;border:none;border-radius:6px;padding:2px 8px;font-size:11px;cursor:pointer;white-space:nowrap;z-index:10;">숨기기</button>`;
|
||||
}
|
||||
h += '</div>';
|
||||
h += `<div class="emp-zone" data-department-id="${dept.id}" style="padding:6px;min-height:36px;">`;
|
||||
if (emps.length) {
|
||||
@@ -343,13 +370,52 @@ function orgChart() {
|
||||
|
||||
handleClick(e) {
|
||||
const btn = e.target.closest('[data-action]');
|
||||
if (!btn) return;
|
||||
if (!btn) {
|
||||
if (this.dblClickDept !== null) { this.dblClickDept = null; this.renderTree(); }
|
||||
return;
|
||||
}
|
||||
if (btn.dataset.action === 'unassign') {
|
||||
e.preventDefault();
|
||||
this.unassignEmployee(parseInt(btn.dataset.empId));
|
||||
} else if (btn.dataset.action === 'hide-dept') {
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
this.hideDept(parseInt(btn.dataset.deptId));
|
||||
} else if (btn.dataset.action === 'restore-dept') {
|
||||
e.preventDefault();
|
||||
this.restoreDept(parseInt(btn.dataset.deptId));
|
||||
}
|
||||
},
|
||||
|
||||
handleDblClick(e) {
|
||||
const header = e.target.closest('[data-action="dept-dblclick"]');
|
||||
if (!header) return;
|
||||
e.preventDefault();
|
||||
const deptId = parseInt(header.dataset.deptId);
|
||||
this.dblClickDept = this.dblClickDept === deptId ? null : deptId;
|
||||
this.renderTree();
|
||||
},
|
||||
|
||||
isDeptHidden(deptId) {
|
||||
if (this.hiddenDepts.has(deptId)) return true;
|
||||
const dept = this.departments.find(d => d.id === deptId);
|
||||
return dept && dept.parent_id ? this.isDeptHidden(dept.parent_id) : false;
|
||||
},
|
||||
|
||||
hideDept(deptId) {
|
||||
this.hiddenDepts.add(deptId);
|
||||
this.dblClickDept = null;
|
||||
this.renderTree();
|
||||
},
|
||||
|
||||
restoreDept(deptId) {
|
||||
this.hiddenDepts.delete(deptId);
|
||||
this.renderTree();
|
||||
},
|
||||
|
||||
get hiddenDeptList() {
|
||||
return this.departments.filter(d => this.hiddenDepts.has(d.id));
|
||||
},
|
||||
|
||||
async saveAssignment(empId, deptId) {
|
||||
this.saving = true;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user