jasan/departments.php
2026-02-08 17:12:56 +09:00

256 lines
No EOL
13 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">
<h3 class="text-2xl font-black mb-6" x-text="isEdit ? '부서 정보 수정' : '신규 부서 생성'"></h3>
<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() {
const action = this.isEdit ? 'update_dept' : 'add_dept';
// Calculate level based on parent
if (this.formData.parent_id) {
const parent = this.depts.find(d => d.id == this.formData.parent_id);
this.formData.level = this.getDeptLevel(parent.path) + 1;
} else {
this.formData.level = 0;
}
fetch(`api.php?action=${action}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.formData)
}).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>