284 lines
No EOL
15 KiB
PHP
284 lines
No EOL
15 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>부서 관리 | FKI ASSET</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
<link href="https://fonts.googleapis.com/css2?family=Pretendard:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="assets/style.css">
|
|
<style>
|
|
[x-cloak] {
|
|
display: none !important;
|
|
}
|
|
|
|
.modal-bg {
|
|
background-color: rgba(15, 23, 42, 0.7);
|
|
backdrop-filter: blur(4px);
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body class="bg-[#f8fafc] text-slate-800" x-data="deptManagement()">
|
|
|
|
<?php include 'nav.php'; ?>
|
|
|
|
<main class="max-w-[1600px] mx-auto p-8 pt-10">
|
|
<header class="mb-10 flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
<div>
|
|
<h2 class="text-3xl font-extrabold text-slate-900 tracking-tight">부서 트리 관리</h2>
|
|
<p class="text-slate-500 mt-1">조직 계층 구조를 시각화하고 부서원을 관리합니다.</p>
|
|
</div>
|
|
<button @click="openAddModal()"
|
|
class="bg-blue-600 text-white px-5 py-2.5 rounded-xl font-bold shadow-lg shadow-blue-200 hover:bg-blue-700 transition-all">신규
|
|
부서 생성</button>
|
|
</header>
|
|
|
|
<!-- Dept Tree Table -->
|
|
<div class="bg-white rounded-3xl shadow-sm border border-slate-200 overflow-hidden">
|
|
<table class="min-w-full divide-y divide-slate-100">
|
|
<thead class="bg-slate-50/50">
|
|
<tr>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">부서명
|
|
(계층구조)</th>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">인원수
|
|
</th>
|
|
<th class="px-6 py-4 text-right text-xs font-bold text-slate-500 uppercase tracking-wider">관리
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-50">
|
|
<template x-for="dept in depts" :key="dept.id">
|
|
<tr class="hover:bg-slate-50/80 transition-colors group">
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="flex items-center">
|
|
<div :style="'margin-left: ' + (getDeptLevel(dept.path) * 20) + 'px'"
|
|
class="flex items-center">
|
|
<div class="w-2 h-2 rounded-full mr-3"
|
|
:class="getDeptLevel(dept.path) === 0 ? 'bg-blue-600' : 'bg-slate-300'">
|
|
</div>
|
|
<span class="text-sm font-bold text-slate-900" x-text="dept.name"></span>
|
|
<span class="ml-2 text-[10px] text-slate-400 font-medium"
|
|
x-show="getDeptLevel(dept.path) > 0" x-text="dept.path"></span>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<button @click="viewMembers(dept)"
|
|
class="text-sm font-black text-blue-600 hover:underline">
|
|
<span x-text="dept.member_count || 0"></span>명 확인
|
|
</button>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-right">
|
|
<button @click="editDept(dept)"
|
|
class="text-xs font-bold text-slate-400 hover:text-blue-600 px-3 py-1 rounded-lg hover:bg-blue-50 transition-all">수정</button>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</main>
|
|
|
|
<!-- Dept Modal -->
|
|
<div x-show="showModal" x-cloak class="fixed inset-0 z-[110] flex items-center justify-center p-4">
|
|
<div class="fixed inset-0 modal-bg" @click="showModal = false"></div>
|
|
<div class="bg-white rounded-3xl p-8 max-w-md w-full relative z-[111] shadow-2xl">
|
|
<div class="flex justify-between items-center mb-6">
|
|
<h3 class="text-2xl font-black text-slate-900" x-text="isEdit ? '부서 정보 수정' : '신규 부서 생성'"></h3>
|
|
<button x-show="isEdit" type="button" @click="deleteDept()"
|
|
class="text-xs font-bold text-rose-500 hover:text-rose-700 bg-rose-50 px-3 py-1.5 rounded-lg border border-rose-100 transition-colors">
|
|
부서 제거
|
|
</button>
|
|
</div>
|
|
<form @submit.prevent="submitDept" class="space-y-4">
|
|
<div>
|
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">부서명</label>
|
|
<input type="text" x-model="formData.name" required
|
|
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none">
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">상위 부서</label>
|
|
<select x-model="formData.parent_id"
|
|
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none">
|
|
<option value="">없음 (최상위)</option>
|
|
<template x-for="d in depts" :key="d.id">
|
|
<option :value="d.id" x-text="d.path" :disabled="d.id == formData.id"></option>
|
|
</template>
|
|
</select>
|
|
</div>
|
|
<div class="pt-4 flex gap-3">
|
|
<button type="button" @click="showModal = false"
|
|
class="flex-1 py-3 bg-slate-100 rounded-xl font-bold">취소</button>
|
|
<button type="submit" class="flex-1 py-3 bg-blue-600 text-white rounded-xl font-bold"
|
|
x-text="isEdit ? '수정 완료' : '부서 생성'"></button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Members Modal -->
|
|
<div x-show="showMembersModal" x-cloak class="fixed inset-0 z-[110] flex items-center justify-center p-4">
|
|
<div class="fixed inset-0 modal-bg" @click="showMembersModal = false"></div>
|
|
<div
|
|
class="bg-white rounded-3xl p-8 max-w-2xl w-full relative z-[111] shadow-2xl overflow-hidden flex flex-col max-h-[80vh]">
|
|
<div class="flex justify-between items-center mb-6">
|
|
<h3 class="text-2xl font-black text-slate-900" x-text="selectedDept?.name + ' 부서원'"></h3>
|
|
<span class="text-xs font-bold text-blue-600 bg-blue-50 px-3 py-1 rounded-full"><span
|
|
x-text="members.length"></span>명</span>
|
|
</div>
|
|
|
|
<div class="flex-1 overflow-y-auto pr-2 no-scrollbar">
|
|
<table class="min-w-full divide-y divide-slate-100">
|
|
<thead class="bg-slate-50 sticky top-0">
|
|
<tr>
|
|
<th class="px-4 py-3 text-left text-xs font-bold text-slate-500">이름</th>
|
|
<th class="px-4 py-3 text-left text-xs font-bold text-slate-500">사번/직위</th>
|
|
<th class="px-4 py-3 text-right text-xs font-bold text-slate-500">이동</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-50">
|
|
<template x-for="user in members" :key="user.id">
|
|
<tr>
|
|
<td class="px-4 py-3 text-sm font-bold text-slate-900" x-text="user.name"></td>
|
|
<td class="px-4 py-3">
|
|
<div class="text-xs font-black text-slate-400" x-text="user.emp_id"></div>
|
|
<div class="text-[10px] text-blue-500 font-bold uppercase"
|
|
x-text="user.position || '-'"></div>
|
|
</td>
|
|
<td class="px-4 py-3 text-right">
|
|
<select @change="reassignMember(user.id, $event.target.value)"
|
|
class="text-xs bg-slate-100 border-none rounded-lg px-2 py-1 focus:ring-2 focus:ring-blue-500">
|
|
<option value="">부서 이동...</option>
|
|
<template x-for="d in depts" :key="d.id">
|
|
<option :value="d.id" x-text="d.name" :disabled="d.id == selectedDept.id">
|
|
</option>
|
|
</template>
|
|
</select>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
<div x-show="members.length === 0" class="py-10 text-center text-slate-400 italic text-sm">소속된 부서원이
|
|
없습니다.</div>
|
|
</div>
|
|
|
|
<div class="mt-6 pt-6 border-t border-slate-100 flex justify-end">
|
|
<button @click="showMembersModal = false"
|
|
class="px-6 py-2.5 bg-slate-900 text-white rounded-xl font-bold">닫기</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function deptManagement() {
|
|
return {
|
|
depts: [],
|
|
showModal: false,
|
|
showMembersModal: false,
|
|
isEdit: false,
|
|
selectedDept: null,
|
|
members: [],
|
|
formData: { id: '', name: '', parent_id: '', level: 1 },
|
|
|
|
init() { this.fetchDepts(); },
|
|
|
|
fetchDepts() {
|
|
const scrollPos = window.scrollY;
|
|
fetch('api.php?action=get_departments').then(res => res.json()).then(data => {
|
|
this.depts = data;
|
|
this.$nextTick(() => window.scrollTo(0, scrollPos));
|
|
});
|
|
},
|
|
|
|
getDeptLevel(path) {
|
|
if (!path) return 0;
|
|
return (path.match(/>/g) || []).length;
|
|
},
|
|
|
|
openAddModal() {
|
|
this.isEdit = false;
|
|
this.formData = { id: '', name: '', parent_id: '', level: 1 };
|
|
this.showModal = true;
|
|
},
|
|
|
|
editDept(dept) {
|
|
this.isEdit = true;
|
|
this.formData = { id: dept.id, name: dept.name, parent_id: dept.parent_id || '', level: dept.level };
|
|
this.showModal = true;
|
|
},
|
|
|
|
submitDept(mergeTargetId = null) {
|
|
const action = this.isEdit ? 'update_dept' : 'add_dept';
|
|
if (!mergeTargetId && this.formData.parent_id) {
|
|
const parent = this.depts.find(d => d.id == this.formData.parent_id);
|
|
this.formData.level = (parent ? this.getDeptLevel(parent.path) : 0) + 1;
|
|
} else if (!mergeTargetId) {
|
|
this.formData.level = 0;
|
|
}
|
|
|
|
const payload = { ...this.formData };
|
|
if (mergeTargetId) payload.merge_target_id = mergeTargetId;
|
|
|
|
fetch(`api.php?action=${action}`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
}).then(res => res.json()).then(data => {
|
|
if (data.success) {
|
|
this.showModal = false;
|
|
this.fetchDepts();
|
|
if (data.merged) {
|
|
alert('부서원이 기존 부서로 이동되었으며, 해당 부서는 삭제되었습니다.');
|
|
}
|
|
} else if (data.conflict) {
|
|
if (confirm(`동일한 위치에 '${this.formData.name}' 부서가 이미 존재합니다.\n부서 정보를 통합하여 부서원을 이동시키겠습니까?`)) {
|
|
this.submitDept(data.existing_id);
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
deleteDept() {
|
|
if (confirm(`정말로 '${this.formData.name}' 부서를 제거하시겠습니까?\n이 명령을 수행하면 소속된 부서원들은 자동으로 '[한국경제인협회] - 미소속' 부서로 이동됩니다.\n이 작업은 되돌릴 수 없습니다.`)) {
|
|
fetch(`api.php?action=delete_dept&id=${this.formData.id}`)
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
this.showModal = false;
|
|
this.fetchDepts();
|
|
}
|
|
});
|
|
}
|
|
},
|
|
|
|
viewMembers(dept) {
|
|
this.selectedDept = dept;
|
|
fetch(`api.php?action=get_dept_users&dept_id=${dept.id}`).then(res => res.json()).then(data => {
|
|
this.members = data;
|
|
this.showMembersModal = true;
|
|
});
|
|
},
|
|
|
|
reassignMember(userId, newDeptId) {
|
|
if (!newDeptId) return;
|
|
fetch('api.php?action=reassign_user', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ user_ids: [userId], new_dept_id: newDeptId })
|
|
}).then(res => res.json()).then(data => {
|
|
if (data.success) {
|
|
this.viewMembers(this.selectedDept); // Refresh list
|
|
this.fetchDepts(); // Refresh member counts
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|