+
+
+
+
+
+
+
+
+
-
-
- 직원을 여기로 드래그하세요
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 직원을 여기로 드래그하세요
-
-
+
+
+
-
+
-
등록된 부서가 없습니다.
-
먼저 부서를 등록해주세요.
+
등록된 부서가 없습니다. 먼저 부서를 등록해주세요.
@@ -194,6 +245,13 @@ class="fixed bottom-6 right-6 bg-gray-800 text-white px-4 py-2 rounded-lg shadow
저장 중...
+
+
@endsection
@push('scripts')
@@ -202,9 +260,12 @@ function orgChart() {
return {
departments: @json($departments),
employees: @json($employees),
+ companyName: @json($companyName),
+ ceoName: @json($ceoName),
searchUnassigned: '',
saving: false,
sortables: [],
+ expandedDepts: [],
get totalEmployees() { return this.employees.length; },
get assignedCount() { return this.employees.filter(e => e.department_id).length; },
@@ -227,16 +288,26 @@ function orgChart() {
return this.employees.filter(e => e.department_id === deptId);
},
- getDeptEmployeeCount(deptId) {
- // 해당 부서 + 하위 부서의 직원 수 합산
+ getDeptTotalCount(deptId) {
let count = this.employees.filter(e => e.department_id === deptId).length;
const children = this.departments.filter(d => d.parent_id === deptId);
for (const child of children) {
- count += this.getDeptEmployeeCount(child.id);
+ count += this.getDeptTotalCount(child.id);
}
return count;
},
+ toggleDept(deptId) {
+ const idx = this.expandedDepts.indexOf(deptId);
+ if (idx >= 0) {
+ this.expandedDepts.splice(idx, 1);
+ } else {
+ this.expandedDepts.push(deptId);
+ }
+ // 하위 부서 드롭존 재초기화
+ this.$nextTick(() => this.initDropZones());
+ },
+
getAvatarColor(name) {
if (!name) return '#9CA3AF';
const colors = ['#6366F1','#8B5CF6','#EC4899','#F59E0B','#10B981','#3B82F6','#EF4444','#14B8A6','#F97316','#6D28D9'];
@@ -246,48 +317,43 @@ function orgChart() {
},
init() {
- this.$nextTick(() => this.initSortable());
+ // 하위 부서가 있는 1단계 부서는 기본 펼침
+ this.expandedDepts = this.rootDepartments
+ .filter(d => this.getChildDepartments(d.id).length > 0)
+ .map(d => d.id);
+
+ this.$nextTick(() => {
+ this.initSortable();
+ this.initDropZones();
+ });
},
initSortable() {
- // 미배치 영역
const unassignedEl = document.getElementById('unassigned-zone');
- if (unassignedEl) {
+ if (unassignedEl && !unassignedEl._sortableInitialized) {
+ unassignedEl._sortableInitialized = true;
this.sortables.push(new Sortable(unassignedEl, {
group: 'org-chart',
animation: 150,
- ghostClass: 'opacity-40',
- dragClass: 'shadow-lg',
+ ghostClass: 'sortable-ghost',
+ dragClass: 'sortable-drag',
onAdd: (evt) => this.handleDrop(evt),
}));
}
-
- // 부서 드롭존 - MutationObserver로 동적 생성 대응
- this.observeAndInitDropZones();
},
- observeAndInitDropZones() {
- const initZones = () => {
- document.querySelectorAll('.dept-drop-zone').forEach(el => {
- if (el._sortableInitialized) return;
- el._sortableInitialized = true;
- this.sortables.push(new Sortable(el, {
- group: 'org-chart',
- animation: 150,
- ghostClass: 'opacity-40',
- dragClass: 'shadow-lg',
- onAdd: (evt) => this.handleDrop(evt),
- }));
- });
- };
-
- initZones();
-
- // Alpine이 DOM을 업데이트할 때 다시 초기화
- const observer = new MutationObserver(() => {
- setTimeout(initZones, 100);
+ initDropZones() {
+ document.querySelectorAll('.dept-drop-zone').forEach(el => {
+ if (el._sortableInitialized) return;
+ el._sortableInitialized = true;
+ this.sortables.push(new Sortable(el, {
+ group: 'org-chart',
+ animation: 150,
+ ghostClass: 'sortable-ghost',
+ dragClass: 'sortable-drag',
+ onAdd: (evt) => this.handleDrop(evt),
+ }));
});
- observer.observe(document.querySelector('[x-data]'), { childList: true, subtree: true });
},
handleDrop(evt) {
@@ -295,13 +361,9 @@ function orgChart() {
const targetDeptId = evt.to.dataset.departmentId;
const newDeptId = targetDeptId ? parseInt(targetDeptId) : null;
- // 클라이언트 상태 업데이트
const emp = this.employees.find(e => e.id === employeeId);
- if (emp) {
- emp.department_id = newDeptId;
- }
+ if (emp) emp.department_id = newDeptId;
- // 서버에 저장
this.saveAssignment(employeeId, newDeptId);
},
@@ -311,7 +373,6 @@ function orgChart() {
const url = departmentId
? '{{ route("rd.org-chart.assign") }}'
: '{{ route("rd.org-chart.unassign") }}';
-
const body = departmentId
? { employee_id: employeeId, department_id: departmentId }
: { employee_id: employeeId };