887 lines
44 KiB
PHP
887 lines
44 KiB
PHP
<?php
|
|
// Just a simple wrapper, mostly static HTML/JS for this SPA architecture
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="th">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>ระบบจัดการผังโครงสร้างหน่วยงาน (Enterprise Organization Chart)</title>
|
|
|
|
<!-- Google Fonts: Sarabun -->
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
|
|
<!-- Favicon -->
|
|
<link rel="icon" type="image/png" href="logo.png">
|
|
|
|
<!-- Tailwind CSS -->
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script>
|
|
tailwind.config = {
|
|
theme: {
|
|
extend: {
|
|
fontFamily: {
|
|
sans: ['Sarabun', 'sans-serif'],
|
|
},
|
|
colors: {
|
|
primary: '#1e40af', // Corporate Blue
|
|
secondary: '#3b82f6', // Light Blue
|
|
surface: '#ffffff',
|
|
background: '#f8fafc', // Very light slate
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<!-- FontAwesome -->
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
|
|
|
<!-- DataTables CSS -->
|
|
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/jquery.dataTables.min.css">
|
|
<link rel="stylesheet" href="https://cdn.datatables.net/responsive/2.5.0/css/responsive.dataTables.min.css">
|
|
|
|
<style>
|
|
body {
|
|
background-color: #f8fafc;
|
|
color: #334155;
|
|
-webkit-font-smoothing: antialiased;
|
|
}
|
|
|
|
.enterprise-card {
|
|
background: #ffffff;
|
|
border: 1px solid #e2e8f0;
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
|
|
border-radius: 0.5rem;
|
|
}
|
|
|
|
/* Navigation Tabs */
|
|
.tab-btn {
|
|
color: #64748b;
|
|
border-bottom: 2px solid transparent;
|
|
transition: all 0.2s ease-in-out;
|
|
padding-bottom: 1rem;
|
|
}
|
|
.tab-btn:hover {
|
|
color: #0f172a;
|
|
border-bottom: 2px solid #cbd5e1;
|
|
}
|
|
.tab-active {
|
|
color: #1e40af !important;
|
|
border-bottom: 2px solid #1e40af !important;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* DataTables Customization - Corporate Look */
|
|
.dataTables_wrapper {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
}
|
|
.dataTables_wrapper .dataTables_filter input {
|
|
border: 1px solid #cbd5e1;
|
|
border-radius: 0.375rem;
|
|
padding: 0.375rem 0.75rem;
|
|
outline: none;
|
|
font-size: 0.875rem;
|
|
transition: border-color 0.15s ease-in-out;
|
|
font-family: inherit;
|
|
}
|
|
.dataTables_wrapper .dataTables_filter input:focus {
|
|
border-color: #3b82f6;
|
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
}
|
|
.dataTables_wrapper .dataTables_length select {
|
|
border: 1px solid #cbd5e1;
|
|
border-radius: 0.375rem;
|
|
padding: 0.25rem 2rem 0.25rem 0.5rem;
|
|
font-size: 0.875rem;
|
|
outline: none;
|
|
font-family: inherit;
|
|
}
|
|
table.dataTable.no-footer {
|
|
border-bottom: 0px !important;
|
|
}
|
|
table.dataTable thead th {
|
|
background-color: #f8fafc;
|
|
color: #64748b;
|
|
font-size: 0.85rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
border-bottom: 1px solid #e2e8f0 !important;
|
|
padding: 1rem 1.5rem !important;
|
|
}
|
|
table.dataTable tbody td {
|
|
padding: 1rem 1.5rem !important;
|
|
border-bottom: 1px solid #f1f5f9;
|
|
vertical-align: middle;
|
|
}
|
|
.dataTables_wrapper .dataTables_paginate .paginate_button.current, .dataTables_wrapper .dataTables_paginate .paginate_button.current:hover {
|
|
background: #EFF6FF !important;
|
|
color: #1e40af !important;
|
|
border: 1px solid #BFDBFE !important;
|
|
border-radius: 0.375rem;
|
|
}
|
|
.dataTables_wrapper .dataTables_paginate .paginate_button {
|
|
border-radius: 0.375rem;
|
|
border: 1px solid transparent !important;
|
|
margin: 0 2px;
|
|
}
|
|
.dataTables_wrapper .dataTables_paginate .paginate_button:hover {
|
|
background: #f1f5f9 !important;
|
|
border: 1px solid #cbd5e1 !important;
|
|
color: #334155 !important;
|
|
}
|
|
|
|
/* D3 Org Chart Node Style */
|
|
.node-rect {
|
|
transition: all 0.2s ease;
|
|
}
|
|
.org-node:hover .node-rect {
|
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05) !important;
|
|
transform: translateY(-2px);
|
|
border-color: #cbd5e1 !important;
|
|
}
|
|
|
|
::-webkit-scrollbar {
|
|
width: 8px;
|
|
height: 8px;
|
|
}
|
|
::-webkit-scrollbar-track {
|
|
background: #f8fafc;
|
|
}
|
|
::-webkit-scrollbar-thumb {
|
|
background: #cbd5e1;
|
|
border-radius: 4px;
|
|
}
|
|
::-webkit-scrollbar-thumb:hover {
|
|
background: #94a3b8;
|
|
}
|
|
|
|
.swal2-popup {
|
|
font-family: 'Sarabun', sans-serif !important;
|
|
}
|
|
|
|
/* Shiny Logo Effect */
|
|
.shiny-logo-container {
|
|
position: relative;
|
|
overflow: hidden;
|
|
border-radius: 50%;
|
|
width: 48px;
|
|
height: 48px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: white;
|
|
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -1px rgba(0,0,0,0.06);
|
|
}
|
|
.shiny-logo-container img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
border-radius: 50%;
|
|
}
|
|
.shiny-logo-container::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: -150%;
|
|
width: 50%;
|
|
height: 100%;
|
|
background: linear-gradient(to right, rgba(255,255,255,0) 0%, rgba(255,255,255,0.7) 50%, rgba(255,255,255,0) 100%);
|
|
transform: skewX(-25deg);
|
|
animation: shine 4s infinite;
|
|
}
|
|
@keyframes shine {
|
|
0% { left: -150%; }
|
|
20% { left: 200%; }
|
|
100% { left: 200%; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="flex flex-col min-h-screen">
|
|
|
|
<!-- Corporate Header -->
|
|
<header class="bg-white border-b border-gray-200 sticky top-0 z-40 shadow-sm">
|
|
<div class="max-w-screen-2xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between h-auto sm:h-16 pt-4 sm:pt-0">
|
|
<div class="flex items-center gap-4 mb-4 sm:mb-0">
|
|
<div class="shiny-logo-container">
|
|
<img src="logo.png" alt="โลโก้หน่วยงาน">
|
|
</div>
|
|
<div>
|
|
<h1 class="text-lg font-bold text-gray-900 leading-tight tracking-wide">ระบบผังโครงสร้างองค์กร</h1>
|
|
<p class="text-[13px] text-gray-500">Enterprise Organization Management</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex space-x-6 h-full items-end pb-0 overflow-x-auto">
|
|
<button id="chartTabBtn" onclick="switchTab('chart')" class="tab-btn tab-active px-2 font-medium flex items-center gap-2 whitespace-nowrap">
|
|
<i class="fas fa-network-wired"></i> แผนผังโครงสร้าง
|
|
</button>
|
|
<button id="dataTabBtn" onclick="switchTab('data')" class="tab-btn px-2 font-medium flex items-center gap-2 whitespace-nowrap text-gray-500">
|
|
<i class="fas fa-address-book"></i> ทะเบียนบุคลากร
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Main Content -->
|
|
<main class="max-w-screen-2xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-6 flex-1 flex flex-col min-h-[calc(100vh-64px)]">
|
|
|
|
<!-- View 1: Chart View -->
|
|
<div id="chart-view" class="flex-1 enterprise-card flex flex-col relative transition-opacity duration-300 min-h-[500px]">
|
|
<div class="px-6 py-4 border-b border-gray-200 flex flex-col xl:flex-row justify-between items-center bg-gray-50/50 rounded-t-lg gap-4">
|
|
<div class="flex flex-wrap items-center gap-3 w-full xl:w-auto">
|
|
<h2 class="text-base font-semibold text-gray-800 flex items-center gap-2 whitespace-nowrap">
|
|
<i class="fas fa-sitemap text-primary"></i> โครงสร้างการบริหารงาน
|
|
</h2>
|
|
<div class="flex items-center gap-2 bg-white px-3 py-1 border border-gray-300 rounded shadow-sm">
|
|
<i class="fas fa-filter text-gray-400 text-sm"></i>
|
|
<select id="deptFilter" onchange="onDeptChange()" class="border-none text-sm outline-none bg-transparent cursor-pointer font-medium text-gray-700 min-w-[150px]">
|
|
<option value="all">กำลังโหลดหน่วยงาน...</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-wrap gap-2 w-full xl:w-auto justify-start xl:justify-end">
|
|
<button onclick="copyLink()" class="text-sm bg-white border border-gray-300 text-gray-700 hover:bg-gray-50 hover:text-blue-600 px-3 py-1.5 rounded shadow-sm transition flex items-center gap-2">
|
|
<i class="fas fa-link text-blue-500"></i> คัดลอกลิงก์
|
|
</button>
|
|
<button onclick="showQRCode()" class="text-sm bg-white border border-gray-300 text-gray-700 hover:bg-gray-50 px-3 py-1.5 rounded shadow-sm transition flex items-center gap-2">
|
|
<i class="fas fa-qrcode text-gray-800"></i> สร้าง QR Code
|
|
</button>
|
|
<button onclick="exportChart()" class="text-sm bg-white border border-gray-300 text-gray-700 hover:bg-gray-50 px-3 py-1.5 rounded shadow-sm transition flex items-center gap-2">
|
|
<i class="fas fa-image text-green-600"></i> ส่งออกรูปภาพ
|
|
</button>
|
|
<div class="w-px h-6 bg-gray-300 mx-1 self-center hidden sm:block"></div>
|
|
<button onclick="if(chart) chart.fit()" class="text-sm bg-white border border-gray-300 text-gray-700 hover:bg-gray-50 px-3 py-1.5 rounded shadow-sm transition flex items-center gap-2">
|
|
<i class="fas fa-compress-arrows-alt"></i> พอดีหน้าจอ
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="chart-container flex-1 w-full overflow-hidden bg-slate-50/30 rounded-b-lg" style="height: 100%; min-height: 500px;"></div>
|
|
</div>
|
|
|
|
<!-- View 2: Data View -->
|
|
<div id="data-view" class="hidden flex-1 enterprise-card flex flex-col transition-opacity duration-300">
|
|
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center bg-white rounded-t-lg flex-wrap gap-4 z-10">
|
|
<h2 class="text-base font-semibold text-gray-800 flex items-center gap-2">
|
|
<i class="fas fa-users text-primary"></i> ฐานข้อมูลบุคลากรทั้งหมด
|
|
</h2>
|
|
<div class="flex gap-2">
|
|
<button onclick="openManageDeptModal()" class="bg-white border border-gray-300 text-gray-700 hover:bg-gray-50 font-medium py-2 px-4 rounded shadow-sm transition-colors flex items-center gap-2 text-sm">
|
|
<i class="fas fa-building text-indigo-500"></i> ตั้งค่าหน่วยงาน
|
|
</button>
|
|
<button onclick="openModal()" class="bg-primary hover:bg-blue-800 text-white font-medium py-2 px-4 rounded shadow-sm transition-colors flex items-center gap-2 text-sm">
|
|
<i class="fas fa-plus"></i> เพิ่มบุคลากรใหม่
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex-1 flex flex-col overflow-hidden bg-white rounded-b-lg">
|
|
<table id="employeeTable" class="w-full text-left" style="width: 100%;">
|
|
<thead>
|
|
<tr>
|
|
<th class="text-left w-20">รูปภาพ</th>
|
|
<th class="text-left">ชื่อ-สกุล</th>
|
|
<th class="text-left">ตำแหน่ง</th>
|
|
<th class="text-left">สังกัด/หน่วยงาน</th>
|
|
<th class="text-center w-24">จัดการ</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="text-sm">
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
</main>
|
|
|
|
<!-- Modal for Employee Form -->
|
|
<div id="empModal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/40 backdrop-blur-sm transition-opacity">
|
|
<div id="empModalContent" class="bg-white rounded-lg shadow-2xl w-full max-w-2xl transform scale-95 opacity-0 transition-all duration-200 border border-gray-200">
|
|
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center bg-gray-50 rounded-t-lg">
|
|
<h3 id="modalTitle" class="text-base font-bold text-gray-800 flex items-center gap-2">
|
|
<i class="fas fa-user-edit text-primary"></i> จัดการข้อมูลบุคลากร
|
|
</h3>
|
|
<button type="button" onclick="closeModal()" class="text-gray-400 hover:text-gray-600 transition-colors w-8 h-8 flex items-center justify-center rounded hover:bg-gray-200">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
<form id="empForm" onsubmit="saveEmployee(event)" class="p-6 space-y-5">
|
|
<input type="hidden" id="empId">
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
|
<div class="space-y-1.5">
|
|
<label class="text-sm font-semibold text-gray-700 block">ชื่อ-สกุล <span class="text-red-500">*</span></label>
|
|
<input type="text" id="empName" required class="w-full px-3 py-2 text-sm rounded border border-gray-300 focus:ring-1 focus:ring-primary focus:border-primary outline-none transition">
|
|
</div>
|
|
<div class="space-y-1.5">
|
|
<label class="text-sm font-semibold text-gray-700 block">ตำแหน่ง <span class="text-red-500">*</span></label>
|
|
<input type="text" id="empPosition" required class="w-full px-3 py-2 text-sm rounded border border-gray-300 focus:ring-1 focus:ring-primary focus:border-primary outline-none transition">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
|
<div class="space-y-1.5">
|
|
<label class="text-sm font-semibold text-gray-700 block">สังกัด/หน่วยงาน <span class="text-red-500">*</span></label>
|
|
<select id="empDepartment" required class="w-full px-3 py-2 text-sm rounded border border-gray-300 focus:ring-1 focus:ring-primary focus:border-primary outline-none transition bg-white">
|
|
<!-- Populated via JS -->
|
|
</select>
|
|
</div>
|
|
<div class="space-y-1.5">
|
|
<label class="text-sm font-semibold text-gray-700 block">รายงานต่อ (ผู้บังคับบัญชา)</label>
|
|
<select id="empParent" class="w-full px-3 py-2 text-sm rounded border border-gray-300 focus:ring-1 focus:ring-primary focus:border-primary outline-none transition bg-white">
|
|
<!-- Populated via JS -->
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-1.5">
|
|
<label class="text-sm font-semibold text-gray-700 flex justify-between">
|
|
<span>URL รูปภาพประจำตัว</span>
|
|
<span class="text-xs text-gray-400 font-normal">(เว้นว่างเพื่อสร้างภาพจำลอง)</span>
|
|
</label>
|
|
<input type="url" id="empImage" placeholder="https://..." class="w-full px-3 py-2 text-sm rounded border border-gray-300 focus:ring-1 focus:ring-primary focus:border-primary outline-none transition">
|
|
</div>
|
|
|
|
<div class="mt-8 pt-5 flex justify-end gap-3 border-t border-gray-100">
|
|
<button type="button" onclick="closeModal()" class="px-4 py-2 rounded text-sm font-medium text-gray-700 bg-white border border-gray-300 hover:bg-gray-50 transition">ยกเลิก</button>
|
|
<button type="submit" class="px-4 py-2 rounded text-sm font-medium text-white bg-primary hover:bg-blue-800 shadow-sm transition">บันทึกข้อมูล</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal for Department Settings -->
|
|
<div id="deptManageModal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/40 backdrop-blur-sm transition-opacity">
|
|
<div id="deptManageModalContent" class="bg-white rounded-lg shadow-2xl w-full max-w-xl transform scale-95 opacity-0 transition-all duration-200 border border-gray-200 flex flex-col max-h-[90vh]">
|
|
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center bg-gray-50 rounded-t-lg">
|
|
<h3 class="text-base font-bold text-gray-800 flex items-center gap-2">
|
|
<i class="fas fa-building text-indigo-500"></i> ตั้งค่าหน่วยงาน
|
|
</h3>
|
|
<button type="button" onclick="closeManageDeptModal()" class="text-gray-400 hover:text-gray-600 transition-colors w-8 h-8 flex items-center justify-center rounded hover:bg-gray-200">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Add Department Form -->
|
|
<div class="p-6 border-b border-gray-100 bg-white">
|
|
<form id="deptForm" onsubmit="saveDepartment(event)" class="flex gap-3">
|
|
<input type="hidden" id="manageDeptId">
|
|
<input type="text" id="manageDeptName" required placeholder="พิมพ์ชื่อหน่วยงานใหม่..." class="flex-1 px-3 py-2 text-sm rounded border border-gray-300 focus:ring-1 focus:ring-primary focus:border-primary outline-none transition">
|
|
<button type="submit" class="px-4 py-2 rounded text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 shadow-sm transition whitespace-nowrap">
|
|
<i class="fas fa-save mr-1"></i> บันทึก
|
|
</button>
|
|
<button type="button" onclick="resetDeptForm()" class="px-3 py-2 rounded text-sm font-medium text-gray-600 bg-gray-100 hover:bg-gray-200 transition" title="ยกเลิก">
|
|
<i class="fas fa-undo"></i>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- List Departments -->
|
|
<div class="p-6 flex-1 overflow-auto bg-slate-50/50 rounded-b-lg">
|
|
<table class="w-full text-left text-sm border-collapse bg-white border border-gray-200 rounded overflow-hidden shadow-sm">
|
|
<thead>
|
|
<tr class="bg-gray-100 border-b border-gray-200 text-gray-700">
|
|
<th class="p-3 font-semibold">ชื่อหน่วยงาน</th>
|
|
<th class="p-3 font-semibold text-center w-24">จัดการ</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="deptTableBody" class="text-gray-700">
|
|
<!-- Loaded via JS -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Scripts -->
|
|
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
|
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
|
|
<script src="https://cdn.datatables.net/responsive/2.5.0/js/dataTables.responsive.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
|
|
|
<!-- D3.js and D3 Org Chart -->
|
|
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/d3-flextree@2.1.2/build/d3-flextree.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/d3-org-chart@3.1.0"></script>
|
|
|
|
<script>
|
|
let orgData = [];
|
|
let departmentsData = [];
|
|
|
|
let chart;
|
|
let dataTable;
|
|
let currentDept = 'all';
|
|
|
|
$(document).ready(function() {
|
|
let urlParams = new URLSearchParams(window.location.search);
|
|
if(urlParams.has('dept')) {
|
|
currentDept = urlParams.get('dept');
|
|
}
|
|
|
|
loadData();
|
|
|
|
window.addEventListener('resize', () => {
|
|
if(chart && !$('#chart-view').hasClass('hidden')) {
|
|
chart.fit();
|
|
}
|
|
});
|
|
});
|
|
|
|
// --- Data Loading ---
|
|
function loadData() {
|
|
// Load Departments
|
|
$.get('api.php?action=get_departments', function(res) {
|
|
if(res.status === 'success') {
|
|
departmentsData = res.data;
|
|
populateDeptDropdowns();
|
|
renderDeptTable();
|
|
}
|
|
});
|
|
|
|
// Load Employees
|
|
$.get('api.php?action=get_employees', function(res) {
|
|
if(res.status === 'success') {
|
|
orgData = res.data;
|
|
renderTable();
|
|
|
|
if(!chart) {
|
|
setTimeout(() => {
|
|
initChart();
|
|
}, 100);
|
|
} else {
|
|
updateChart();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// --- Departments Logic ---
|
|
function populateDeptDropdowns() {
|
|
let selectFilter = $('#deptFilter');
|
|
let selectForm = $('#empDepartment');
|
|
|
|
selectFilter.empty();
|
|
selectFilter.append('<option value="all">แสดงทุกหน่วยงาน</option>');
|
|
selectForm.empty();
|
|
selectForm.append('<option value="">-- เลือกหน่วยงาน --</option>');
|
|
|
|
departmentsData.forEach(d => {
|
|
selectFilter.append(`<option value="${d.name}">${d.name}</option>`);
|
|
selectForm.append(`<option value="${d.id}">${d.name}</option>`);
|
|
});
|
|
|
|
if (departmentsData.some(d => d.name === currentDept)) {
|
|
selectFilter.val(currentDept);
|
|
} else {
|
|
currentDept = 'all';
|
|
selectFilter.val('all');
|
|
}
|
|
}
|
|
|
|
function renderDeptTable() {
|
|
let tbody = $('#deptTableBody');
|
|
tbody.empty();
|
|
|
|
if(departmentsData.length === 0) {
|
|
tbody.append(`<tr><td colspan="2" class="p-4 text-center text-gray-500">ยังไม่มีข้อมูลหน่วยงาน</td></tr>`);
|
|
return;
|
|
}
|
|
|
|
departmentsData.forEach(d => {
|
|
tbody.append(`
|
|
<tr class="border-b border-gray-100 hover:bg-gray-50 transition">
|
|
<td class="p-3">${d.name}</td>
|
|
<td class="p-3 text-center">
|
|
<div class="flex gap-1 justify-center">
|
|
<button onclick="editDepartment('${d.id}', '${d.name}')" class="w-7 h-7 rounded bg-white border border-gray-200 text-gray-600 hover:text-blue-600 hover:border-blue-300 flex items-center justify-center transition-all" title="แก้ไข">
|
|
<i class="fas fa-pen text-[10px]"></i>
|
|
</button>
|
|
<button onclick="deleteDepartment('${d.id}')" class="w-7 h-7 rounded bg-white border border-gray-200 text-gray-600 hover:text-red-600 hover:border-red-300 flex items-center justify-center transition-all" title="ลบ">
|
|
<i class="fas fa-trash text-[10px]"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`);
|
|
});
|
|
}
|
|
|
|
function openManageDeptModal() {
|
|
resetDeptForm();
|
|
$('#deptManageModal').removeClass('hidden').addClass('flex');
|
|
setTimeout(() => {
|
|
$('#deptManageModalContent').removeClass('scale-95 opacity-0').addClass('scale-100 opacity-100');
|
|
}, 10);
|
|
}
|
|
|
|
function closeManageDeptModal() {
|
|
$('#deptManageModalContent').removeClass('scale-100 opacity-100').addClass('scale-95 opacity-0');
|
|
setTimeout(() => {
|
|
$('#deptManageModal').removeClass('flex').addClass('hidden');
|
|
}, 200);
|
|
}
|
|
|
|
function resetDeptForm() {
|
|
$('#manageDeptId').val('');
|
|
$('#manageDeptName').val('');
|
|
}
|
|
|
|
function editDepartment(id, name) {
|
|
$('#manageDeptId').val(id);
|
|
$('#manageDeptName').val(name);
|
|
$('#manageDeptName').focus();
|
|
}
|
|
|
|
function saveDepartment(e) {
|
|
e.preventDefault();
|
|
let data = {
|
|
id: $('#manageDeptId').val(),
|
|
name: $('#manageDeptName').val()
|
|
};
|
|
|
|
$.post('api.php?action=save_department', data, function(res) {
|
|
if(res.status === 'success') {
|
|
resetDeptForm();
|
|
loadData(); // refresh all
|
|
Swal.fire({icon: 'success', title: 'บันทึกหน่วยงานสำเร็จ', timer: 1500, showConfirmButton: false});
|
|
} else {
|
|
Swal.fire('ข้อผิดพลาด', res.message, 'error');
|
|
}
|
|
}, 'json');
|
|
}
|
|
|
|
function deleteDepartment(id) {
|
|
Swal.fire({
|
|
title: 'ยืนยันการลบหน่วยงาน?',
|
|
text: "หากมีบุคลากรในหน่วยงานนี้ จะไม่สามารถลบได้",
|
|
icon: 'warning',
|
|
showCancelButton: true,
|
|
confirmButtonColor: '#dc2626',
|
|
cancelButtonColor: '#64748b',
|
|
confirmButtonText: 'ลบ',
|
|
cancelButtonText: 'ยกเลิก'
|
|
}).then((result) => {
|
|
if (result.isConfirmed) {
|
|
$.post('api.php?action=delete_department', {id: id}, function(res) {
|
|
if(res.status === 'success') {
|
|
loadData();
|
|
Swal.fire({icon: 'success', title: 'ลบสำเร็จ', timer: 1500, showConfirmButton: false});
|
|
} else {
|
|
Swal.fire('ข้อผิดพลาด', res.message, 'error');
|
|
}
|
|
}, 'json');
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
// --- Employee / Chart Logic ---
|
|
function onDeptChange() {
|
|
currentDept = $('#deptFilter').val();
|
|
let url = new URL(window.location);
|
|
if (currentDept === 'all') {
|
|
url.searchParams.delete('dept');
|
|
} else {
|
|
url.searchParams.set('dept', currentDept);
|
|
}
|
|
window.history.pushState({}, '', url);
|
|
updateChart();
|
|
}
|
|
|
|
function getShareUrl() {
|
|
let url = new URL(window.location.href);
|
|
let dept = $('#deptFilter').val();
|
|
if (dept === 'all') {
|
|
url.searchParams.delete('dept');
|
|
} else {
|
|
url.searchParams.set('dept', dept);
|
|
}
|
|
return url.toString();
|
|
}
|
|
|
|
function copyLink() {
|
|
let url = getShareUrl();
|
|
navigator.clipboard.writeText(url).then(() => {
|
|
Swal.fire({icon: 'success', title: 'คัดลอกลิงก์สำเร็จ', timer: 1500, showConfirmButton: false});
|
|
}).catch(err => {
|
|
prompt('กรุณาคัดลอกลิงก์นี้:', url);
|
|
});
|
|
}
|
|
|
|
function showQRCode() {
|
|
let url = getShareUrl();
|
|
let dept = $('#deptFilter').val();
|
|
let deptName = dept === 'all' ? 'ทุกหน่วยงาน' : dept;
|
|
let fileName = dept === 'all' ? 'QR_OrgChart_All.png' : `QR_OrgChart_${dept}.png`;
|
|
let qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${encodeURIComponent(url)}`;
|
|
|
|
Swal.fire({
|
|
title: `QR Code ผัง${deptName}`,
|
|
html: `<div class="flex justify-center my-4"><img src="${qrUrl}" class="w-56 h-56 border border-gray-200 p-2 rounded-lg bg-white shadow-sm"></div>`,
|
|
showCancelButton: true,
|
|
confirmButtonText: '<i class="fas fa-download mr-1"></i> โหลด QR Code',
|
|
cancelButtonText: 'ปิด',
|
|
confirmButtonColor: '#1e40af',
|
|
}).then((result) => {
|
|
if (result.isConfirmed) {
|
|
fetch(qrUrl).then(response => response.blob()).then(blob => {
|
|
const blobUrl = URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = blobUrl; link.download = fileName; link.click();
|
|
URL.revokeObjectURL(blobUrl);
|
|
}).catch(e => window.open(qrUrl, '_blank'));
|
|
}
|
|
});
|
|
}
|
|
|
|
function exportChart() {
|
|
if (!chart) return;
|
|
let dept = $('#deptFilter').val();
|
|
let fileName = dept === 'all' ? 'Organization_Chart_All' : `Organization_Chart_${dept}`;
|
|
chart.exportImg({ full: true, filename: fileName });
|
|
}
|
|
|
|
function getFilteredChartData() {
|
|
let dept = $('#deptFilter').val() || currentDept;
|
|
let chartData = [];
|
|
|
|
if (dept === 'all') {
|
|
chartData = JSON.parse(JSON.stringify(orgData));
|
|
chartData.forEach(d => {
|
|
if (!d.parentId) d.parentId = ''; // Ensure d3 root format
|
|
});
|
|
} else {
|
|
let filtered = orgData.filter(e => e.department === dept);
|
|
let filteredIds = new Set(filtered.map(e => e.id));
|
|
chartData = JSON.parse(JSON.stringify(filtered));
|
|
|
|
chartData.forEach(d => {
|
|
if (!filteredIds.has(d.parentId)) d.parentId = 'dummy_root';
|
|
});
|
|
|
|
chartData.push({
|
|
id: 'dummy_root',
|
|
parentId: '',
|
|
name: `แผนก: ${dept}`,
|
|
position: 'โครงสร้างหน่วยงาน',
|
|
department: dept,
|
|
imageUrl: `https://ui-avatars.com/api/?name=${encodeURIComponent(dept)}&background=1e40af&color=fff&size=150`
|
|
});
|
|
}
|
|
return chartData;
|
|
}
|
|
|
|
function initChart() {
|
|
try {
|
|
if(orgData.length === 0) {
|
|
document.querySelector('.chart-container').innerHTML = '<div class="p-8 text-center text-gray-500">ยังไม่มีข้อมูลบุคลากรในระบบ</div>';
|
|
return;
|
|
}
|
|
document.querySelector('.chart-container').innerHTML = '';
|
|
|
|
let dataToRender = getFilteredChartData();
|
|
chart = new d3.OrgChart()
|
|
.container('.chart-container')
|
|
.data(dataToRender)
|
|
.nodeId((d) => d.id)
|
|
.parentNodeId((d) => d.parentId)
|
|
.nodeWidth((d) => 280)
|
|
.nodeHeight((d) => 120)
|
|
.childrenMargin((d) => 50)
|
|
.compactMarginBetween((d) => 25)
|
|
.compactMarginPair((d) => 60)
|
|
.nodeContent(function(d, i, arr, state) {
|
|
return `
|
|
<div class="org-node" style="width: 280px; height: 120px; padding-top: 10px;">
|
|
<div class="node-rect" style="background-color: white; border: 1px solid #cbd5e1; border-top: 4px solid #1e40af; border-radius: 6px; height: 100%; display: flex; align-items: center; padding: 14px; box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);">
|
|
<div style="flex-shrink: 0; margin-right: 14px;">
|
|
<img src="${d.data.imageUrl}" style="width: 60px; height: 60px; border-radius: 6px; object-fit: cover; border: 1px solid #e2e8f0; background-color: #f8fafc;" />
|
|
</div>
|
|
<div style="flex-grow: 1; min-width: 0; text-align: left;">
|
|
<div style="font-family: 'Sarabun', sans-serif; font-size: 15px; font-weight: 600; color: #0f172a; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${d.data.name}">
|
|
${d.data.name}
|
|
</div>
|
|
<div style="font-family: 'Sarabun', sans-serif; font-size: 13px; color: #475569; margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${d.data.position}">
|
|
${d.data.position}
|
|
</div>
|
|
<div style="margin-top: 6px;">
|
|
<span style="font-family: 'Sarabun', sans-serif; font-size: 11px; background-color: #f1f5f9; color: #475569; padding: 2px 8px; border-radius: 4px; border: 1px solid #e2e8f0; display: inline-block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%;">
|
|
${d.data.department}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
chart.render();
|
|
setTimeout(() => { chart.expandAll().render().fit(); }, 100);
|
|
} catch (error) {
|
|
console.error("OrgChart Error:", error);
|
|
document.querySelector('.chart-container').innerHTML = `<div class="p-8 text-center text-red-500">Error: ${error.message}</div>`;
|
|
}
|
|
}
|
|
|
|
function updateChart() {
|
|
if(chart) {
|
|
try {
|
|
let dataToRender = getFilteredChartData();
|
|
if(dataToRender.length === 0) {
|
|
document.querySelector('.chart-container').innerHTML = '<div class="p-8 text-center text-gray-500">ไม่มีข้อมูลหน่วยงานนี้</div>';
|
|
} else {
|
|
chart.data(dataToRender).render().fit();
|
|
}
|
|
} catch(error) {}
|
|
}
|
|
}
|
|
|
|
function switchTab(tab) {
|
|
if (tab === 'chart') {
|
|
$('#chartTabBtn').addClass('tab-active').removeClass('text-gray-500');
|
|
$('#dataTabBtn').removeClass('tab-active').addClass('text-gray-500');
|
|
$('#chart-view').removeClass('hidden').addClass('flex');
|
|
$('#data-view').addClass('hidden').removeClass('flex');
|
|
setTimeout(() => { if (chart) chart.render().fit(); }, 50);
|
|
} else {
|
|
$('#dataTabBtn').addClass('tab-active').removeClass('text-gray-500');
|
|
$('#chartTabBtn').removeClass('tab-active').addClass('text-gray-500');
|
|
$('#data-view').removeClass('hidden').addClass('flex');
|
|
$('#chart-view').addClass('hidden').removeClass('flex');
|
|
}
|
|
}
|
|
|
|
function renderTable() {
|
|
if ($.fn.DataTable.isDataTable('#employeeTable')) {
|
|
$('#employeeTable').DataTable().destroy();
|
|
}
|
|
const tbody = $('#employeeTable tbody');
|
|
tbody.empty();
|
|
|
|
orgData.forEach(emp => {
|
|
let parentName = '-';
|
|
if(emp.parentId) {
|
|
let parent = orgData.find(e => e.id === emp.parentId);
|
|
if(parent) parentName = parent.name;
|
|
}
|
|
|
|
tbody.append(`
|
|
<tr class="hover:bg-slate-50 transition-colors group">
|
|
<td><img src="${emp.imageUrl}" class="w-10 h-10 rounded-full object-cover border border-gray-200 bg-white"></td>
|
|
<td>
|
|
<div class="font-semibold text-gray-900">${emp.name}</div>
|
|
<div class="text-[12px] text-gray-500 mt-0.5">รายงานต่อ: ${parentName}</div>
|
|
</td>
|
|
<td class="text-gray-700">${emp.position}</td>
|
|
<td><span class="bg-gray-100 text-gray-700 px-2.5 py-1 rounded-md text-[12px] font-medium border border-gray-200">${emp.department}</span></td>
|
|
<td class="text-center">
|
|
<div class="flex gap-2 justify-center opacity-0 group-hover:opacity-100 transition-opacity">
|
|
<button onclick="openModal('${emp.id}')" class="w-8 h-8 rounded bg-white border border-gray-200 text-gray-600 hover:text-blue-600 hover:border-blue-300 flex items-center justify-center transition-all"><i class="fas fa-pen text-[11px]"></i></button>
|
|
<button onclick="deleteEmployee('${emp.id}')" class="w-8 h-8 rounded bg-white border border-gray-200 text-gray-600 hover:text-red-600 hover:border-red-300 flex items-center justify-center transition-all"><i class="fas fa-trash text-[11px]"></i></button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`);
|
|
});
|
|
|
|
dataTable = $('#employeeTable').DataTable({
|
|
responsive: true, autoWidth: false, pageLength: 10,
|
|
language: { url: '//cdn.datatables.net/plug-ins/1.13.6/i18n/th.json' },
|
|
dom: '<"flex flex-wrap justify-between items-center px-6 py-4 border-b border-gray-100 bg-white gap-4"lf>rt<"flex flex-wrap justify-between items-center px-6 py-4 bg-gray-50 border-t border-gray-200 gap-4 mt-auto"ip>'
|
|
});
|
|
}
|
|
|
|
function openModal(id = null) {
|
|
let parentSelect = $('#empParent');
|
|
parentSelect.empty();
|
|
parentSelect.append('<option value="">-- ไม่มี (ระดับสูงสุด) --</option>');
|
|
|
|
orgData.forEach(emp => {
|
|
if(emp.id !== id) {
|
|
parentSelect.append(`<option value="${emp.id}">${emp.name} (${emp.position})</option>`);
|
|
}
|
|
});
|
|
|
|
if (id) {
|
|
let emp = orgData.find(e => e.id === id);
|
|
$('#empId').val(emp.id);
|
|
$('#empName').val(emp.name);
|
|
$('#empPosition').val(emp.position);
|
|
$('#empDepartment').val(emp.department_id);
|
|
$('#empImage').val(emp.imageUrl);
|
|
$('#empParent').val(emp.parentId);
|
|
$('#modalTitle').html('<i class="fas fa-user-edit text-primary"></i> แก้ไขข้อมูลบุคลากร');
|
|
} else {
|
|
$('#empForm')[0].reset();
|
|
$('#empId').val('');
|
|
$('#modalTitle').html('<i class="fas fa-user-plus text-primary"></i> เพิ่มบุคลากรใหม่');
|
|
}
|
|
|
|
$('#empModal').removeClass('hidden').addClass('flex');
|
|
setTimeout(() => { $('#empModalContent').removeClass('scale-95 opacity-0').addClass('scale-100 opacity-100'); }, 10);
|
|
}
|
|
|
|
function closeModal() {
|
|
$('#empModalContent').removeClass('scale-100 opacity-100').addClass('scale-95 opacity-0');
|
|
setTimeout(() => { $('#empModal').removeClass('flex').addClass('hidden'); }, 200);
|
|
}
|
|
|
|
function saveEmployee(e) {
|
|
e.preventDefault();
|
|
|
|
let data = {
|
|
id: $('#empId').val(),
|
|
name: $('#empName').val(),
|
|
position: $('#empPosition').val(),
|
|
department_id: $('#empDepartment').val(),
|
|
image_url: $('#empImage').val(),
|
|
parent_id: $('#empParent').val()
|
|
};
|
|
|
|
$.post('api.php?action=save_employee', data, function(res) {
|
|
if(res.status === 'success') {
|
|
closeModal();
|
|
loadData();
|
|
Swal.fire({icon: 'success', title: 'บันทึกสำเร็จ', timer: 1500, showConfirmButton: false});
|
|
} else {
|
|
Swal.fire('ข้อผิดพลาด', res.message, 'error');
|
|
}
|
|
}, 'json');
|
|
}
|
|
|
|
function deleteEmployee(id) {
|
|
Swal.fire({
|
|
title: 'ยืนยันการลบ?',
|
|
text: "ลบข้อมูลบุคลากรนี้ใช่หรือไม่?",
|
|
icon: 'warning',
|
|
showCancelButton: true,
|
|
confirmButtonColor: '#dc2626',
|
|
cancelButtonColor: '#64748b',
|
|
confirmButtonText: 'ลบ',
|
|
cancelButtonText: 'ยกเลิก'
|
|
}).then((result) => {
|
|
if (result.isConfirmed) {
|
|
$.post('api.php?action=delete_employee', {id: id}, function(res) {
|
|
if(res.status === 'success') {
|
|
loadData();
|
|
Swal.fire({icon: 'success', title: 'ลบสำเร็จ', timer: 1500, showConfirmButton: false});
|
|
} else {
|
|
Swal.fire('ข้อผิดพลาด', res.message, 'error');
|
|
}
|
|
}, 'json');
|
|
}
|
|
});
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|