Initial commit

This commit is contained in:
Porawit Dongwang
2026-09-16 23:20:08 +07:00
commit 0041668dbb
32577 changed files with 3687927 additions and 0 deletions
@@ -0,0 +1,886 @@
<?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>