1722 lines
87 KiB
PHP
1722 lines
87 KiB
PHP
<?php
|
|
session_start();
|
|
require_once 'config.php';
|
|
header('Content-Type: text/html; charset=utf-8'); // Override JSON header from config.php
|
|
|
|
if (isset($_GET['logout'])) {
|
|
session_destroy();
|
|
header("Location: index.php");
|
|
exit;
|
|
}
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['password'])) {
|
|
if ($_POST['password'] === APP_PASSWORD) {
|
|
$_SESSION['logged_in'] = true;
|
|
header("Location: index.php");
|
|
exit;
|
|
} else {
|
|
$error = "รหัสผ่านไม่ถูกต้อง";
|
|
}
|
|
}
|
|
|
|
if (empty($_SESSION['logged_in'])) {
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="th">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>เข้าสู่ระบบ - ระบบจัดตารางเวร</title>
|
|
<link rel="icon" href="logo.png" type="image/png">
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
<style>body { font-family: 'Sarabun', sans-serif; }</style>
|
|
</head>
|
|
<body class="bg-slate-100 min-h-screen flex items-center justify-center p-4">
|
|
<div class="bg-white rounded-xl shadow-lg p-8 w-full max-w-md">
|
|
<div class="flex justify-center mb-6">
|
|
<img src="logo.png" alt="Logo" class="w-24 h-24 object-contain rounded-full shadow-sm">
|
|
</div>
|
|
<h1 class="text-2xl font-bold text-center text-blue-900 mb-6">เข้าสู่ระบบจัดตารางเวร</h1>
|
|
|
|
<?php if (isset($error)): ?>
|
|
<div class="bg-red-50 border border-red-200 text-red-600 text-sm px-4 py-3 rounded-md mb-4 text-center">
|
|
<?php echo $error; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<form method="POST" class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-700 mb-1">รหัสผ่าน</label>
|
|
<input type="password" name="password" required autofocus
|
|
class="w-full border border-slate-300 rounded-md px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
</div>
|
|
<button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-md transition-colors">
|
|
เข้าสู่ระบบ
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
<?php
|
|
exit;
|
|
}
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="th">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>ระบบจัดตารางเวร - ศูนย์คอมพิวเตอร์</title>
|
|
<link rel="icon" href="logo.png" type="image/png">
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@300;400;500;600;700&display=swap"
|
|
rel="stylesheet">
|
|
<style>
|
|
/* Shiny Logo Effect */
|
|
.logo-shiny {
|
|
position: relative;
|
|
overflow: hidden;
|
|
border-radius: 50%;
|
|
display: inline-block;
|
|
}
|
|
|
|
.logo-shiny::before {
|
|
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.8) 50%, rgba(255,255,255,0) 100%);
|
|
transform: skewX(-25deg);
|
|
animation: shine 3s infinite;
|
|
z-index: 10;
|
|
}
|
|
|
|
@keyframes shine {
|
|
0% { left: -150%; }
|
|
50% { left: 150%; }
|
|
100% { left: 150%; }
|
|
}
|
|
|
|
body {
|
|
font-family: 'Sarabun', sans-serif;
|
|
background-color: #f3f4f6;
|
|
}
|
|
|
|
/* Custom scrollbar for table container */
|
|
.table-container::-webkit-scrollbar {
|
|
height: 8px;
|
|
width: 8px;
|
|
}
|
|
|
|
.table-container::-webkit-scrollbar-track {
|
|
background: #f1f1f1;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.table-container::-webkit-scrollbar-thumb {
|
|
background: #cbd5e1;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.table-container::-webkit-scrollbar-thumb:hover {
|
|
background: #94a3b8;
|
|
}
|
|
|
|
/* Cell styles */
|
|
.cell-weekend {
|
|
background-color: #e2e8f0;
|
|
/* bg-slate-200 */
|
|
}
|
|
|
|
.cell-holiday {
|
|
background-color: #fca5a5;
|
|
/* bg-red-300 */
|
|
}
|
|
|
|
.text-circle {
|
|
display: inline-block;
|
|
width: 28px;
|
|
height: 28px;
|
|
line-height: 24px;
|
|
text-align: center;
|
|
border-radius: 50%;
|
|
border: 2px solid #334155;
|
|
font-weight: bold;
|
|
}
|
|
|
|
/* Shift colors */
|
|
.shift-m {
|
|
color: #2563eb;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Morning: Blue */
|
|
.shift-a {
|
|
color: #16a34a;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Afternoon: Green */
|
|
.shift-ma {
|
|
color: #9333ea;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Morning/Afternoon: Purple */
|
|
/* Leave: Red */
|
|
.shift-l {
|
|
color: #dc2626;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Meeting: Orange */
|
|
.shift-meet {
|
|
color: #f97316;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Loading overlay */
|
|
#loadingOverlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(255,255,255,0.7);
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
z-index: 9999;
|
|
font-size: 1.5rem;
|
|
color: #334155;
|
|
}
|
|
.hidden-overlay {
|
|
display: none !important;
|
|
}
|
|
|
|
.exporting .hide-on-export {
|
|
display: none !important;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body class="p-4 md:p-8">
|
|
<div id="loadingOverlay" class="hidden-overlay">
|
|
กำลังโหลดข้อมูล...
|
|
</div>
|
|
|
|
<div class="max-w-7xl mx-auto bg-white rounded-xl shadow-lg p-6 mb-8 relative">
|
|
<a href="?logout=1"
|
|
class="absolute top-4 right-4 sm:top-6 sm:right-6 whitespace-nowrap bg-red-50 hover:bg-red-100 text-red-600 border border-red-200 px-3 py-1.5 sm:px-4 sm:py-2 rounded-lg text-xs sm:text-sm transition-all shadow-sm flex items-center gap-2 hover:shadow group">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 transform group-hover:translate-x-0.5 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
|
</svg>
|
|
<span class="hidden sm:inline">ออกจากระบบ</span>
|
|
</a>
|
|
<div class="flex flex-col items-center mb-6">
|
|
<div class="logo-shiny mb-4 w-24 h-24 sm:w-32 sm:h-32">
|
|
<img src="logo.png" alt="Logo" class="w-full h-full object-contain">
|
|
</div>
|
|
<h1 class="text-2xl md:text-3xl font-bold text-center text-slate-800 mb-2">
|
|
แบบฟอร์มตารางเวร ประจำเดือน <span id="monthYearDisplay"></span>
|
|
</h1>
|
|
<h2 class="text-lg text-center text-slate-600">งานศูนย์คอมพิวเตอร์ ฝ่ายคอมพิวเตอร์และสารสนเทศ</h2>
|
|
</div>
|
|
|
|
<div class="flex flex-col md:flex-row justify-between items-center mb-6 gap-4">
|
|
<div class="flex gap-2">
|
|
<select id="monthSelect"
|
|
class="border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
<option value="0">มกราคม</option>
|
|
<option value="1">กุมภาพันธ์</option>
|
|
<option value="2">มีนาคม</option>
|
|
<option value="3">เมษายน</option>
|
|
<option value="4">พฤษภาคม</option>
|
|
<option value="5">มิถุนายน</option>
|
|
<option value="6">กรกฎาคม</option>
|
|
<option value="7">สิงหาคม</option>
|
|
<option value="8">กันยายน</option>
|
|
<option value="9">ตุลาคม</option>
|
|
<option value="10">พฤศจิกายน</option>
|
|
<option value="11">ธันวาคม</option>
|
|
</select>
|
|
<input type="number" id="yearSelect" value=""
|
|
class="border border-gray-300 rounded-md px-3 py-2 text-sm w-24 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
<button id="updateMonthBtn"
|
|
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm transition-colors shadow-sm">อัปเดต</button>
|
|
</div>
|
|
|
|
<div class="flex gap-2 text-sm flex-wrap justify-center">
|
|
<span class="inline-flex items-center gap-1"><span class="w-3 h-3 rounded-full bg-blue-600"></span> เช้า
|
|
(ช)</span>
|
|
<span class="inline-flex items-center gap-1"><span class="w-3 h-3 rounded-full bg-green-600"></span>
|
|
บ่าย (บ)</span>
|
|
<span class="inline-flex items-center gap-1"><span class="w-3 h-3 rounded-full bg-purple-600"></span>
|
|
เช้า/บ่าย (ช/บ)</span>
|
|
<span class="inline-flex items-center gap-1"><span class="w-3 h-3 rounded-full bg-red-600"></span> ลา
|
|
(ลา)</span>
|
|
<span class="inline-flex items-center gap-1"><span class="w-3 h-3 rounded-full bg-orange-500"></span> ประชุม (ป)</span>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2 flex-wrap justify-center">
|
|
<button id="toggleLockBtn" onclick="toggleMonthLock()"
|
|
class="bg-slate-200 hover:bg-slate-300 text-slate-700 p-2 rounded-md transition-colors shadow-sm flex items-center justify-center" title="ล็อกเดือนนี้">
|
|
<svg id="lockIcon" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 11V7a4 4 0 118 0m-4 8v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2z" />
|
|
</svg>
|
|
<span id="lockText" class="hidden">ล็อกเดือนนี้</span>
|
|
</button>
|
|
<button onclick="autoGenerateSchedule()"
|
|
class="whitespace-nowrap bg-amber-500 hover:bg-amber-600 text-white px-4 py-2 rounded-md text-sm transition-colors shadow-sm flex items-center gap-2">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" />
|
|
</svg>
|
|
จัดเวรอัตโนมัติ
|
|
</button>
|
|
<button id="exportImageBtn"
|
|
class="whitespace-nowrap bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-md text-sm transition-colors shadow-sm flex items-center gap-2">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
|
</svg>
|
|
ส่งออกเป็นรูปภาพ
|
|
</button>
|
|
<button onclick="openDocuments()"
|
|
class="whitespace-nowrap bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-md text-sm transition-colors shadow-sm flex items-center gap-2">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
เอกสาร
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add an ID to the container we want to capture -->
|
|
<div id="captureArea" class="bg-white p-2">
|
|
<!-- Title for image export (hidden by default, shown during export) -->
|
|
<div id="exportHeader" class="hidden flex-col items-center mb-6">
|
|
<div class="mb-4 w-24 h-24 sm:w-32 sm:h-32">
|
|
<img src="logo.png" alt="Logo" class="w-full h-full object-contain">
|
|
</div>
|
|
<h1 class="text-2xl font-bold text-slate-800 text-center mb-2">
|
|
แบบฟอร์มตารางเวร ประจำเดือน <span id="exportMonthYear"></span>
|
|
</h1>
|
|
<h2 class="text-lg text-slate-600 text-center">งานศูนย์คอมพิวเตอร์ ฝ่ายคอมพิวเตอร์และสารสนเทศ</h2>
|
|
</div>
|
|
|
|
<!-- Shift Summary Box -->
|
|
<div id="shiftSummary" class="flex gap-4 mb-4 justify-end text-sm hide-on-export">
|
|
<div class="bg-blue-50 text-blue-800 px-3 py-1.5 rounded-md border border-blue-200 shadow-sm flex items-center gap-2">
|
|
<span class="w-3 h-3 rounded-full bg-blue-600"></span>
|
|
เวรเช้าคงเหลือ: <span id="remainMorning" class="font-bold text-lg leading-none">0</span> / <span id="totalMorning">0</span>
|
|
</div>
|
|
<div class="bg-green-50 text-green-800 px-3 py-1.5 rounded-md border border-green-200 shadow-sm flex items-center gap-2">
|
|
<span class="w-3 h-3 rounded-full bg-green-600"></span>
|
|
เวรบ่ายคงเหลือ: <span id="remainAfternoon" class="font-bold text-lg leading-none">0</span> / <span id="totalAfternoon">0</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="overflow-x-auto table-container border border-slate-300 rounded-lg">
|
|
<table class="w-full text-sm text-center border-collapse whitespace-nowrap" id="scheduleTable">
|
|
<thead class="bg-slate-100 text-slate-700">
|
|
<tr id="daysHeaderRow">
|
|
<!-- จะถูกสร้างโดย JavaScript -->
|
|
</tr>
|
|
</thead>
|
|
<tbody id="scheduleBody">
|
|
<!-- จะถูกสร้างโดย JavaScript -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- ข้อมูลเพิ่มเติมส่วนท้ายตาราง (อ้างอิงตามรูปภาพตัวอย่าง) -->
|
|
<div class="mt-8 px-4 text-sm text-slate-700">
|
|
<div class="overflow-x-auto shadow-sm rounded-lg border border-slate-300">
|
|
<table class="w-full text-left border-collapse bg-white whitespace-nowrap">
|
|
<thead class="bg-blue-50 text-slate-800">
|
|
<tr>
|
|
<th class="py-3 px-4 border border-slate-300 font-bold" colspan="2">
|
|
<div class="flex items-center justify-between text-blue-900">
|
|
<div class="flex items-center gap-2">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
<path d="M2 3a1 1 0 011-1h2.153a1 1 0 01.986.836l.74 4.435a1 1 0 01-.54 1.06l-1.548.773a11.037 11.037 0 006.105 6.105l.774-1.548a1 1 0 011.059-.54l4.435.74a1 1 0 01.836.986V17a1 1 0 01-1 1h-2C7.82 18 2 12.18 2 5V3z" />
|
|
</svg>
|
|
เวลา 24.00 - 08.00 น. สามารถติดต่อได้ที่
|
|
</div>
|
|
<button onclick="openAddStaffModal()" class="hide-on-export text-xs bg-blue-600 hover:bg-blue-700 text-white py-1 px-2 rounded flex items-center gap-1 transition-colors">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
|
|
</svg>
|
|
เพิ่มบุคคล
|
|
</button>
|
|
</div>
|
|
</th>
|
|
<th class="py-2 px-3 border border-slate-300 font-bold text-center bg-slate-100" colspan="5">
|
|
ผู้ติดต่อหลักประจำวัน
|
|
<span class="block text-[10px] font-normal text-slate-500">* คลิกช่องด้านล่างเพื่อเปลี่ยน</span>
|
|
</th>
|
|
</tr>
|
|
<tr class="bg-slate-50 text-center">
|
|
<th class="py-2 px-4 border border-slate-300 w-12 text-slate-500 font-medium">ลำดับ</th>
|
|
<th class="py-2 px-4 border border-slate-300 text-left font-medium">ชื่อ - เบอร์โทรศัพท์</th>
|
|
<th class="py-2 px-3 border border-slate-300 font-medium w-12 bg-slate-100">จ</th>
|
|
<th class="py-2 px-3 border border-slate-300 font-medium w-12 bg-slate-100">อ</th>
|
|
<th class="py-2 px-3 border border-slate-300 font-medium w-12 bg-slate-100">พ</th>
|
|
<th class="py-2 px-3 border border-slate-300 font-medium w-12 bg-slate-100">พฤ</th>
|
|
<th class="py-2 px-3 border border-slate-300 font-medium w-12 bg-slate-100">ศ</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="contactAndOncallBody">
|
|
<!-- จะถูกสร้างโดย JavaScript -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div> <!-- ปิด captureArea -->
|
|
</div> <!-- ปิด max-w-7xl -->
|
|
|
|
<!-- Edit Shift Modal -->
|
|
<div id="editModal" class="fixed inset-0 bg-black/50 hidden flex items-center justify-center z-50">
|
|
<div class="bg-white rounded-lg shadow-xl p-6 w-96 max-w-[90%]">
|
|
<h3 class="text-lg font-bold mb-4" id="modalTitle">กำหนดเวร</h3>
|
|
<p class="text-sm text-slate-600 mb-4" id="modalSubtitle"></p>
|
|
|
|
<div class="space-y-3">
|
|
<label class="flex items-center space-x-3 p-2 rounded hover:bg-slate-50 cursor-pointer">
|
|
<input type="radio" name="shiftType" value="" class="form-radio text-blue-600" checked>
|
|
<span>ไม่มีเวร (ว่าง)</span>
|
|
</label>
|
|
<label class="flex items-center space-x-3 p-2 rounded hover:bg-slate-50 cursor-pointer"
|
|
id="morningOption">
|
|
<input type="radio" name="shiftType" value="ช" class="form-radio text-blue-600">
|
|
<span class="font-medium text-blue-600">เวรเช้า (ช)</span>
|
|
</label>
|
|
<label class="flex items-center space-x-3 p-2 rounded hover:bg-slate-50 cursor-pointer">
|
|
<input type="radio" name="shiftType" value="บ" class="form-radio text-green-600">
|
|
<span class="font-medium text-green-600">เวรบ่าย (บ)</span>
|
|
</label>
|
|
<label class="flex items-center space-x-3 p-2 rounded hover:bg-slate-50 cursor-pointer" id="bothOption">
|
|
<input type="radio" name="shiftType" value="ช/บ" class="form-radio text-purple-600">
|
|
<span class="font-medium text-purple-600">เวรเช้าและบ่าย (ช/บ)</span>
|
|
</label>
|
|
<label class="flex items-center space-x-3 p-2 rounded hover:bg-slate-50 cursor-pointer">
|
|
<input type="radio" name="shiftType" value="ลา" class="form-radio text-red-600">
|
|
<span class="font-medium text-red-600">ลาพัก/ลากิจ (ลา)</span>
|
|
</label>
|
|
<label class="flex items-center space-x-3 p-2 rounded hover:bg-slate-50 cursor-pointer">
|
|
<input type="radio" name="shiftType" value="ป" class="form-radio text-orange-500">
|
|
<span class="font-medium text-orange-500">ประชุม (ป)</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="mt-6 flex justify-end gap-2">
|
|
<button id="cancelModalBtn"
|
|
class="px-4 py-2 text-slate-600 hover:bg-slate-100 rounded-md transition-colors">ยกเลิก</button>
|
|
<button id="saveModalBtn"
|
|
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors flex items-center">
|
|
<span id="saveModalText">บันทึก</span>
|
|
<svg id="saveModalSpinner" class="animate-spin -ml-1 mr-3 h-5 w-5 text-white hidden" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="mt-4 text-sm text-slate-500 italic border-t pt-4">
|
|
* คลิกที่เซลล์ในตารางเพื่อกำหนดเวร
|
|
<br>
|
|
* คลิกที่ชื่อพนักงานเพื่อแก้ไขชื่อ
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit Name Modal -->
|
|
<div id="editNameModal" class="fixed inset-0 bg-black/50 hidden flex items-center justify-center z-50">
|
|
<div class="bg-white rounded-lg shadow-xl p-6 w-96 max-w-[90%]">
|
|
<h3 class="text-lg font-bold mb-4">แก้ไขชื่อพนักงาน</h3>
|
|
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">ชื่อ - นามสกุล</label>
|
|
<input type="text" id="editNameInput"
|
|
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">เบอร์โทรศัพท์</label>
|
|
<input type="text" id="editPhoneInput" placeholder="เช่น 081-234-5678"
|
|
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
</div>
|
|
|
|
<hr class="border-gray-200">
|
|
|
|
<div class="bg-blue-50 rounded p-3">
|
|
<p class="text-xs font-bold text-blue-900 mb-2">ตั้งค่าสำหรับตัวช่วยจัดเวรอัตโนมัติ</p>
|
|
|
|
<div class="mb-3">
|
|
<label class="block text-xs font-medium text-gray-700 mb-1">วันปกติ (จ.-ศ.) ที่ต้องการเวรบ่าย:</label>
|
|
<div class="flex flex-wrap gap-2">
|
|
<label class="inline-flex items-center"><input type="checkbox" name="pref_weekdays" value="1" class="form-checkbox h-4 w-4 text-blue-600"><span class="ml-1 text-sm">จ.</span></label>
|
|
<label class="inline-flex items-center"><input type="checkbox" name="pref_weekdays" value="2" class="form-checkbox h-4 w-4 text-blue-600"><span class="ml-1 text-sm">อ.</span></label>
|
|
<label class="inline-flex items-center"><input type="checkbox" name="pref_weekdays" value="3" class="form-checkbox h-4 w-4 text-blue-600"><span class="ml-1 text-sm">พ.</span></label>
|
|
<label class="inline-flex items-center"><input type="checkbox" name="pref_weekdays" value="4" class="form-checkbox h-4 w-4 text-blue-600"><span class="ml-1 text-sm">พฤ.</span></label>
|
|
<label class="inline-flex items-center"><input type="checkbox" name="pref_weekdays" value="5" class="form-checkbox h-4 w-4 text-blue-600"><span class="ml-1 text-sm">ศ.</span></label>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-700 mb-1">วันหยุด (ส.-อา., นักขัตฤกษ์) กะที่ต้องการ:</label>
|
|
<div class="flex flex-wrap gap-3">
|
|
<label class="inline-flex items-center"><input type="radio" name="pref_weekend" value="" class="form-radio h-4 w-4 text-blue-600" checked><span class="ml-1 text-sm">ไม่ระบุ</span></label>
|
|
<label class="inline-flex items-center"><input type="radio" name="pref_weekend" value="ช" class="form-radio h-4 w-4 text-blue-600"><span class="ml-1 text-sm">เช้า</span></label>
|
|
<label class="inline-flex items-center"><input type="radio" name="pref_weekend" value="บ" class="form-radio h-4 w-4 text-blue-600"><span class="ml-1 text-sm">บ่าย</span></label>
|
|
<label class="inline-flex items-center"><input type="radio" name="pref_weekend" value="ช/บ" class="form-radio h-4 w-4 text-blue-600"><span class="ml-1 text-sm">เช้า/บ่าย</span></label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-6 flex justify-between gap-2">
|
|
<button id="deleteStaffBtn" onclick="deleteStaff()" class="px-4 py-2 text-red-600 hover:bg-red-50 border border-red-200 rounded-md transition-colors">ลบรายชื่อ</button>
|
|
<div class="flex gap-2">
|
|
<button id="cancelNameModalBtn"
|
|
class="px-4 py-2 text-slate-600 hover:bg-slate-100 rounded-md transition-colors">ยกเลิก</button>
|
|
<button id="saveNameModalBtn"
|
|
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors flex items-center">
|
|
<span id="saveNameModalText">บันทึก</span>
|
|
<svg id="saveNameModalSpinner" class="animate-spin -ml-1 mr-3 h-5 w-5 text-white hidden" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Conflict Warning Modal -->
|
|
<div id="conflictModal" class="fixed inset-0 bg-black/50 hidden flex items-center justify-center z-50">
|
|
<div class="bg-white rounded-lg shadow-xl p-6 w-[28rem] max-w-[90%] transform transition-all">
|
|
<div class="flex items-start gap-4">
|
|
<div class="flex-shrink-0 bg-amber-100 rounded-full p-2">
|
|
<svg class="h-6 w-6 text-amber-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-lg font-bold text-slate-900 mb-2">แจ้งเตือนเวรซ้ำซ้อน</h3>
|
|
<p class="text-sm text-slate-600 mb-4">วันดังกล่าวมีการจัดเวรให้บุคคลอื่นแล้ว:</p>
|
|
<div id="conflictStaffList" class="bg-amber-50 border border-amber-200 rounded text-sm text-amber-800 p-3 mb-4 space-y-1">
|
|
<!-- รายชื่อผู้มีเวรซ้ำซ้อน -->
|
|
</div>
|
|
<p class="text-sm font-medium text-slate-700">คุณแน่ใจหรือไม่ว่าต้องการบันทึกเวรซ้อนกัน?</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-6 flex justify-end gap-3">
|
|
<button id="cancelConflictBtn" onclick="closeConflictModal()"
|
|
class="px-4 py-2 text-slate-600 hover:bg-slate-100 rounded-md transition-colors text-sm font-medium">ยกเลิก</button>
|
|
<button id="confirmConflictBtn" onclick="forceSaveShift()"
|
|
class="px-4 py-2 bg-amber-600 hover:bg-amber-700 text-white rounded-md transition-colors text-sm font-medium flex items-center">
|
|
<span id="confirmConflictText">ยืนยันบันทึก</span>
|
|
<svg id="confirmConflictSpinner" class="animate-spin -ml-1 mr-2 h-4 w-4 text-white hidden" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Lock Warning Modal -->
|
|
<div id="lockAlertModal" class="fixed inset-0 bg-black/50 hidden flex items-center justify-center z-[9999]">
|
|
<div class="bg-white rounded-lg shadow-xl p-6 w-[28rem] max-w-[90%] transform transition-all">
|
|
<div class="flex items-start gap-4">
|
|
<div class="flex-shrink-0 bg-red-100 rounded-full p-2">
|
|
<svg class="h-6 w-6 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-lg font-bold text-slate-900 mb-2">แจ้งเตือน</h3>
|
|
<p id="lockAlertMessage" class="text-sm text-slate-600 mb-6">เดือนนี้ถูกล็อกไว้ ไม่สามารถแก้ไขได้</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-2 flex justify-end">
|
|
<button onclick="closeLockAlert()"
|
|
class="px-4 py-2 bg-slate-600 hover:bg-slate-700 text-white rounded-md transition-colors text-sm font-medium">
|
|
ตกลง
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Toast Container -->
|
|
<div id="toastContainer" class="fixed top-4 right-4 z-[9999] flex flex-col gap-2 pointer-events-none"></div>
|
|
|
|
<!-- SweetAlert2 library -->
|
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
|
|
|
<!-- html2canvas library -->
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
|
|
|
|
<script>
|
|
// App State
|
|
let staffList = [];
|
|
let scheduleData = {};
|
|
|
|
// Custom holidays (Format: 'YYYY-MM-DD')
|
|
let customHolidays = [];
|
|
let fetchedHolidayYear = null;
|
|
|
|
let currentMonth = new Date().getMonth(); // 0-11
|
|
let currentYear = new Date().getFullYear(); // AD
|
|
let currentEditingCell = null;
|
|
let currentEditingStaffId = null;
|
|
|
|
// Elements
|
|
const daysHeaderRow = document.getElementById('daysHeaderRow');
|
|
const scheduleBody = document.getElementById('scheduleBody');
|
|
const monthYearDisplay = document.getElementById('monthYearDisplay');
|
|
const monthSelect = document.getElementById('monthSelect');
|
|
const yearSelect = document.getElementById('yearSelect');
|
|
const updateMonthBtn = document.getElementById('updateMonthBtn');
|
|
const exportImageBtn = document.getElementById('exportImageBtn');
|
|
const editModal = document.getElementById('editModal');
|
|
const modalTitle = document.getElementById('modalTitle');
|
|
const modalSubtitle = document.getElementById('modalSubtitle');
|
|
const cancelModalBtn = document.getElementById('cancelModalBtn');
|
|
const saveModalBtn = document.getElementById('saveModalBtn');
|
|
const loadingOverlay = document.getElementById('loadingOverlay');
|
|
const saveModalText = document.getElementById('saveModalText');
|
|
const saveModalSpinner = document.getElementById('saveModalSpinner');
|
|
|
|
// Name Edit Modal Elements
|
|
const editNameModal = document.getElementById('editNameModal');
|
|
const editNameInput = document.getElementById('editNameInput');
|
|
const cancelNameModalBtn = document.getElementById('cancelNameModalBtn');
|
|
const saveNameModalBtn = document.getElementById('saveNameModalBtn');
|
|
const saveNameModalText = document.getElementById('saveNameModalText');
|
|
const saveNameModalSpinner = document.getElementById('saveNameModalSpinner');
|
|
|
|
// Options hiding/showing based on day type
|
|
const morningOption = document.getElementById('morningOption');
|
|
const bothOption = document.getElementById('bothOption');
|
|
|
|
let currentIsLocked = false;
|
|
|
|
function showLockAlert(message) {
|
|
document.getElementById('lockAlertMessage').textContent = message || 'เดือนนี้ถูกล็อกไว้ ไม่สามารถแก้ไขได้';
|
|
document.getElementById('lockAlertModal').classList.remove('hidden');
|
|
}
|
|
|
|
function closeLockAlert() {
|
|
document.getElementById('lockAlertModal').classList.add('hidden');
|
|
}
|
|
|
|
function showToast(message, type = 'warning') {
|
|
const container = document.getElementById('toastContainer');
|
|
if (!container) return;
|
|
|
|
const toast = document.createElement('div');
|
|
// Base classes
|
|
let classes = "flex items-center w-full max-w-xs p-4 text-gray-500 bg-white rounded-lg shadow dark:text-gray-400 dark:bg-gray-800 pointer-events-auto transform transition-all duration-300 translate-y-[-1rem] opacity-0";
|
|
|
|
let iconHtml = '';
|
|
if (type === 'warning') {
|
|
iconHtml = `<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-orange-500 bg-orange-100 rounded-lg dark:bg-orange-700 dark:text-orange-200">
|
|
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM10 15a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm1-4a1 1 0 0 1-2 0V6a1 1 0 0 1 2 0v5Z"/>
|
|
</svg>
|
|
<span class="sr-only">Warning icon</span>
|
|
</div>`;
|
|
} else if (type === 'success') {
|
|
iconHtml = `<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-green-500 bg-green-100 rounded-lg dark:bg-green-800 dark:text-green-200">
|
|
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 8.207-4 4a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L9 10.586l3.293-3.293a1 1 0 0 1 1.414 1.414Z"/>
|
|
</svg>
|
|
<span class="sr-only">Check icon</span>
|
|
</div>`;
|
|
}
|
|
|
|
toast.className = classes;
|
|
toast.innerHTML = `
|
|
${iconHtml}
|
|
<div class="ml-3 text-sm font-normal text-slate-800 font-sarabun">${message}</div>
|
|
`;
|
|
|
|
container.appendChild(toast);
|
|
|
|
// Trigger animation
|
|
requestAnimationFrame(() => {
|
|
toast.classList.remove('translate-y-[-1rem]', 'opacity-0');
|
|
});
|
|
|
|
// Remove after 3 seconds
|
|
setTimeout(() => {
|
|
toast.classList.add('opacity-0', 'translate-y-[-1rem]');
|
|
setTimeout(() => {
|
|
if (container.contains(toast)) {
|
|
container.removeChild(toast);
|
|
}
|
|
}, 300);
|
|
}, 3000);
|
|
}
|
|
|
|
async function fetchMonthLock(year, month) {
|
|
try {
|
|
const res = await fetch(`api/locks.php?year=${year}&month=${month}`);
|
|
const result = await res.json();
|
|
if (result.status === 'success') {
|
|
currentIsLocked = result.is_locked;
|
|
updateLockUI();
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
}
|
|
|
|
function updateLockUI() {
|
|
const btn = document.getElementById('toggleLockBtn');
|
|
const icon = document.getElementById('lockIcon');
|
|
const text = document.getElementById('lockText');
|
|
if (!btn) return;
|
|
|
|
if (currentIsLocked) {
|
|
btn.className = "bg-red-500 hover:bg-red-600 text-white p-2 rounded-md transition-colors shadow-sm flex items-center justify-center";
|
|
btn.title = "ปลดล็อกเดือนนี้";
|
|
if(text) text.textContent = "ปลดล็อกเดือนนี้";
|
|
// closed lock icon
|
|
icon.innerHTML = `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />`;
|
|
} else {
|
|
btn.className = "bg-slate-200 hover:bg-slate-300 text-slate-700 p-2 rounded-md transition-colors shadow-sm flex items-center justify-center";
|
|
btn.title = "ล็อกเดือนนี้";
|
|
if(text) text.textContent = "ล็อกเดือนนี้";
|
|
// open lock icon
|
|
icon.innerHTML = `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 11V7a4 4 0 118 0m-4 8v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2z" />`;
|
|
}
|
|
}
|
|
|
|
async function toggleMonthLock() {
|
|
try {
|
|
showLoading();
|
|
const newState = !currentIsLocked;
|
|
const res = await fetch('api/locks.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
year: currentYear,
|
|
month: currentMonth + 1,
|
|
is_locked: newState
|
|
})
|
|
});
|
|
const result = await res.json();
|
|
if (result.status === 'success') {
|
|
currentIsLocked = result.is_locked;
|
|
updateLockUI();
|
|
showToast(currentIsLocked ? 'ล็อกเดือนนี้เรียบร้อยแล้ว' : 'ปลดล็อกเดือนนี้เรียบร้อยแล้ว', 'success');
|
|
} else {
|
|
showToast('เกิดข้อผิดพลาด: ' + result.message, 'warning');
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
showToast('เกิดข้อผิดพลาดในการเชื่อมต่อ', 'warning');
|
|
} finally {
|
|
hideLoading();
|
|
}
|
|
}
|
|
|
|
// Initialize UI with current date
|
|
monthSelect.value = currentMonth;
|
|
yearSelect.value = currentYear + 543;
|
|
|
|
// Fetch Initial Data
|
|
async function initApp() {
|
|
showLoading();
|
|
await fetchStaff();
|
|
await fetchOncall();
|
|
await fetchHolidays(currentYear);
|
|
await fetchMonthLock(currentYear, currentMonth + 1);
|
|
await fetchSchedules(currentYear, currentMonth + 1);
|
|
hideLoading();
|
|
renderSchedule();
|
|
renderContactAndOncall();
|
|
}
|
|
|
|
function formatPhoneNumber(phone) {
|
|
if (!phone) return '-';
|
|
// Strip all non-digit characters
|
|
const cleaned = ('' + phone).replace(/\D/g, '');
|
|
// Format 10 digit numbers (mobile)
|
|
if (cleaned.length === 10) {
|
|
return `${cleaned.slice(0,3)}-${cleaned.slice(3,6)}-${cleaned.slice(6)}`;
|
|
}
|
|
// Format 9 digit numbers (landline)
|
|
if (cleaned.length === 9) {
|
|
return `${cleaned.slice(0,2)}-${cleaned.slice(2,5)}-${cleaned.slice(5)}`;
|
|
}
|
|
// Return original if it doesn't match standard Thai lengths
|
|
return phone;
|
|
}
|
|
|
|
function renderContactAndOncall() {
|
|
const tbody = document.getElementById('contactAndOncallBody');
|
|
let html = '';
|
|
|
|
staffList.forEach((staff, index) => {
|
|
const phoneFormatted = formatPhoneNumber(staff.phone);
|
|
const nameCleaned = staff.name.replace(/^(นาย|นางสาว|นาง)\s*/, '');
|
|
|
|
html += `<tr class="hover:bg-blue-50 transition-colors">`;
|
|
|
|
// Column 1: Number
|
|
html += `<td class="py-2 px-4 text-center border border-slate-300 align-middle">
|
|
<span class="inline-flex items-center justify-center bg-blue-200 text-blue-800 text-xs font-bold w-5 h-5 rounded-full">${index + 1}</span>
|
|
</td>`;
|
|
|
|
// Column 2: Name & Phone
|
|
html += `<td class="py-2 px-4 border border-slate-300 align-middle">
|
|
<div class="flex items-center justify-between min-w-[200px]">
|
|
<span class="font-medium text-slate-700">คุณ${nameCleaned}</span>
|
|
<a href="tel:${staff.phone ? staff.phone.replace(/\D/g, '') : ''}" class="text-blue-600 font-semibold tracking-wide hover:underline ml-4">${phoneFormatted}</a>
|
|
</div>
|
|
</td>`;
|
|
|
|
// Columns 3-7: Oncall Days (Mon-Fri)
|
|
for (let day = 1; day <= 5; day++) {
|
|
const isOncall = parseInt(oncallData[day]) === parseInt(staff.id);
|
|
const mark = isOncall ? '<span class="text-blue-600 font-bold">/</span>' : '';
|
|
const bgClass = isOncall ? 'bg-blue-50' : 'cursor-pointer hover:bg-blue-100';
|
|
|
|
html += `<td class="border border-slate-300 px-3 w-12 text-center align-middle transition-colors ${bgClass}"
|
|
onclick="handleOncallClick(${staff.id}, ${day})">${mark}</td>`;
|
|
}
|
|
|
|
html += `</tr>`;
|
|
});
|
|
|
|
tbody.innerHTML = html;
|
|
}
|
|
|
|
async function fetchHolidays(year) {
|
|
if (fetchedHolidayYear === year) return; // Already fetched for this year
|
|
|
|
try {
|
|
const response = await fetch(`api/holidays.php?year=${year}`);
|
|
const result = await response.json();
|
|
if (result.status === 'success') {
|
|
customHolidays = result.data;
|
|
fetchedHolidayYear = year;
|
|
} else {
|
|
console.error('Failed to load holidays', result.message);
|
|
}
|
|
} catch (error) {
|
|
console.error('API Error:', error);
|
|
}
|
|
}
|
|
|
|
let oncallData = {};
|
|
|
|
async function fetchOncall() {
|
|
try {
|
|
const response = await fetch('api/oncall.php');
|
|
const result = await response.json();
|
|
if (result.status === 'success') {
|
|
oncallData = result.data;
|
|
}
|
|
} catch (error) {
|
|
console.error('API Error:', error);
|
|
}
|
|
}
|
|
|
|
async function handleOncallClick(staffId, dayOfWeek) {
|
|
if (currentIsLocked) {
|
|
showLockAlert('เดือนนี้ถูกล็อกไว้ ไม่สามารถแก้ไขผู้ติดต่อหลักได้');
|
|
return;
|
|
}
|
|
|
|
// Optimistic update
|
|
oncallData[dayOfWeek] = staffId;
|
|
renderContactAndOncall();
|
|
|
|
try {
|
|
const response = await fetch('api/oncall.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ day_of_week: dayOfWeek, staff_id: staffId })
|
|
});
|
|
const result = await response.json();
|
|
if (result.status !== 'success') {
|
|
Swal.fire('ข้อผิดพลาด', 'เกิดข้อผิดพลาดในการบันทึกผู้ติดต่อหลัก: ' + result.message, 'error');
|
|
await fetchOncall(); // revert on fail
|
|
renderContactAndOncall();
|
|
}
|
|
} catch (error) {
|
|
console.error('API Error:', error);
|
|
await fetchOncall();
|
|
renderContactAndOncall();
|
|
}
|
|
}
|
|
|
|
async function fetchStaff() {
|
|
try {
|
|
const response = await fetch('api/staff.php');
|
|
const result = await response.json();
|
|
if (result.status === 'success') {
|
|
staffList = result.data;
|
|
} else {
|
|
console.error('Failed to load staff', result.message);
|
|
}
|
|
} catch (error) {
|
|
console.error('API Error:', error);
|
|
}
|
|
}
|
|
|
|
async function fetchSchedules(year, month) {
|
|
try {
|
|
const response = await fetch(`api/schedules.php?year=${year}&month=${month}`);
|
|
const result = await response.json();
|
|
if (result.status === 'success') {
|
|
scheduleData = result.data;
|
|
// Normalize data just in case
|
|
for (const staffId in scheduleData) {
|
|
for (const day in scheduleData[staffId]) {
|
|
if (scheduleData[staffId][day] === 'ชบ') {
|
|
scheduleData[staffId][day] = 'ช/บ';
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
console.error('Failed to load schedules', result.message);
|
|
}
|
|
} catch (error) {
|
|
console.error('API Error:', error);
|
|
}
|
|
}
|
|
|
|
function showLoading() {
|
|
loadingOverlay.classList.remove('hidden-overlay');
|
|
}
|
|
|
|
function hideLoading() {
|
|
loadingOverlay.classList.add('hidden-overlay');
|
|
}
|
|
|
|
// Get number of days in month
|
|
function getDaysInMonth(month, year) {
|
|
return new Date(year, month + 1, 0).getDate();
|
|
}
|
|
|
|
// Check if weekend
|
|
function isWeekend(day, month, year) {
|
|
const date = new Date(year, month, day);
|
|
const dayOfWeek = date.getDay();
|
|
return dayOfWeek === 0 || dayOfWeek === 6; // Sunday(0) or Saturday(6)
|
|
}
|
|
|
|
// Check if custom holiday
|
|
function isHoliday(day, month, year) {
|
|
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
|
// If customHolidays is an object, use hasOwnProperty. If array, use includes (backward compat just in case)
|
|
if (Array.isArray(customHolidays)) return customHolidays.includes(dateStr);
|
|
return customHolidays.hasOwnProperty(dateStr);
|
|
}
|
|
|
|
function getHolidayName(day, month, year) {
|
|
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
|
if (Array.isArray(customHolidays)) return 'วันหยุด';
|
|
return customHolidays[dateStr] || 'วันหยุด';
|
|
}
|
|
|
|
// Calculate shift count
|
|
function calculateShiftCount(staffId) {
|
|
let count = 0;
|
|
const staffSchedule = scheduleData[staffId] || {};
|
|
for (const day in staffSchedule) {
|
|
const shift = staffSchedule[day];
|
|
if (shift === 'ช' || shift === 'บ') {
|
|
count += 1;
|
|
} else if (shift === 'ช/บ') {
|
|
count += 2; // Count as 2 shifts
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
function renderSchedule() {
|
|
const daysInMonth = getDaysInMonth(currentMonth, currentYear);
|
|
const thaiMonths = ["มกราคม", "กุมภาพันธ์", "มีนาคม", "เมษายน", "พฤษภาคม", "มิถุนายน", "กรกฎาคม", "สิงหาคม", "กันยายน", "ตุลาคม", "พฤศจิกายน", "ธันวาคม"];
|
|
|
|
// Update title
|
|
monthYearDisplay.textContent = `${thaiMonths[currentMonth]} ${currentYear + 543}`;
|
|
|
|
// 1. Render Headers
|
|
let headerHTML = `<th class="border border-slate-300 p-2 w-[160px] min-w-[160px] max-w-[160px] sticky left-0 bg-slate-100 z-20 truncate">ชื่อ - นามสกุล</th>`;
|
|
headerHTML += `<th class="border border-slate-300 p-2 w-[70px] min-w-[70px] max-w-[70px] sticky left-[160px] bg-slate-100 z-20">จำนวน</th>`;
|
|
|
|
for (let day = 1; day <= daysInMonth; day++) {
|
|
const weekend = isWeekend(day, currentMonth, currentYear);
|
|
const holiday = isHoliday(day, currentMonth, currentYear);
|
|
let holidayTitleAttr = "";
|
|
|
|
let thClass = "border border-slate-300 p-2 w-10 min-w-[40px]";
|
|
|
|
if (weekend) {
|
|
thClass += " cell-weekend";
|
|
} else if (holiday) {
|
|
thClass += " cell-holiday";
|
|
holidayTitleAttr = `title="${getHolidayName(day, currentMonth, currentYear)}"`;
|
|
}
|
|
|
|
if (weekend || holiday) {
|
|
// Use SVG for flawless html2canvas rendering
|
|
headerHTML += `<th class="${thClass}" ${holidayTitleAttr}>
|
|
<svg width="28" height="28" style="display: block; margin: 0 auto;">
|
|
<circle cx="14" cy="14" r="12" stroke="#334155" stroke-width="2" fill="none" />
|
|
<text x="14" y="14" text-anchor="middle" dy=".35em" font-weight="bold" fill="#334155" font-family="Sarabun, sans-serif" font-size="14px">${day}</text>
|
|
</svg>
|
|
</th>`;
|
|
} else {
|
|
headerHTML += `<th class="${thClass}">${day}</th>`;
|
|
}
|
|
}
|
|
daysHeaderRow.innerHTML = headerHTML;
|
|
|
|
// 2. Render Body
|
|
let bodyHTML = '';
|
|
staffList.forEach(staff => {
|
|
const shiftCount = calculateShiftCount(staff.id);
|
|
bodyHTML += `<tr>`;
|
|
bodyHTML += `<td class="border border-slate-300 p-2 text-left w-[160px] min-w-[160px] max-w-[160px] sticky left-0 bg-white z-10 font-medium cursor-pointer hover:bg-slate-50 transition-colors align-middle truncate" data-action="edit-name" data-staff-id="${staff.id}" title="${staff.name}">${staff.name}</td>`;
|
|
bodyHTML += `<td class="border border-slate-300 p-2 font-bold align-middle text-center w-[70px] min-w-[70px] max-w-[70px] sticky left-[160px] bg-white z-10" id="count-${staff.id}">${shiftCount}</td>`;
|
|
|
|
for (let day = 1; day <= daysInMonth; day++) {
|
|
const weekend = isWeekend(day, currentMonth, currentYear);
|
|
const holiday = isHoliday(day, currentMonth, currentYear);
|
|
let tdClass = "border border-slate-300 p-1 cursor-pointer hover:bg-blue-50 transition-colors h-10 align-middle text-center";
|
|
let holidayTitleAttr = "";
|
|
|
|
if (weekend) tdClass += " cell-weekend hover:bg-slate-300";
|
|
if (holiday) {
|
|
tdClass += " cell-holiday hover:bg-red-400";
|
|
holidayTitleAttr = `title="${getHolidayName(day, currentMonth, currentYear)}"`;
|
|
}
|
|
|
|
const shift = scheduleData[staff.id]?.[day] || '';
|
|
let displayShift = shift;
|
|
let shiftClass = 'inline-block align-middle leading-none mt-1';
|
|
|
|
if (shift === 'ช') shiftClass += ' shift-m';
|
|
else if (shift === 'บ') shiftClass += ' shift-a';
|
|
else if (shift === 'ช/บ') shiftClass += ' shift-ma text-xs';
|
|
else if (shift === 'ลา') shiftClass += ' shift-l text-xs';
|
|
else if (shift === 'ป') shiftClass += ' shift-meet text-xs';
|
|
|
|
bodyHTML += `<td class="${tdClass}" ${holidayTitleAttr}
|
|
data-staff-id="${staff.id}"
|
|
data-day="${day}"
|
|
data-weekend="${weekend || holiday}">
|
|
<span class="${shiftClass}">${displayShift}</span>
|
|
</td>`;
|
|
}
|
|
bodyHTML += `</tr>`;
|
|
});
|
|
|
|
scheduleBody.innerHTML = bodyHTML;
|
|
|
|
// Attach click events
|
|
const shiftCells = scheduleBody.querySelectorAll('td[data-day]');
|
|
shiftCells.forEach(cell => {
|
|
cell.addEventListener('click', handleCellClick);
|
|
});
|
|
|
|
const nameCells = scheduleBody.querySelectorAll('td[data-action="edit-name"]');
|
|
nameCells.forEach(cell => {
|
|
cell.addEventListener('click', handleNameClick);
|
|
});
|
|
|
|
updateShiftSummary();
|
|
}
|
|
|
|
function updateShiftSummary() {
|
|
const daysInMonth = getDaysInMonth(currentMonth, currentYear);
|
|
let totalMorning = 0;
|
|
let totalAfternoon = daysInMonth;
|
|
|
|
for (let day = 1; day <= daysInMonth; day++) {
|
|
if (isWeekend(day, currentMonth, currentYear) || isHoliday(day, currentMonth, currentYear)) {
|
|
totalMorning++;
|
|
}
|
|
}
|
|
|
|
let usedMorning = 0;
|
|
let usedAfternoon = 0;
|
|
|
|
for (const staffId in scheduleData) {
|
|
for (const day in scheduleData[staffId]) {
|
|
const shift = scheduleData[staffId][day];
|
|
if (shift === 'ช') {
|
|
usedMorning++;
|
|
} else if (shift === 'บ') {
|
|
usedAfternoon++;
|
|
} else if (shift === 'ช/บ') {
|
|
usedMorning++;
|
|
usedAfternoon++;
|
|
}
|
|
}
|
|
}
|
|
|
|
const remainMorning = totalMorning - usedMorning;
|
|
const remainAfternoon = totalAfternoon - usedAfternoon;
|
|
|
|
document.getElementById('totalMorning').textContent = totalMorning;
|
|
document.getElementById('remainMorning').textContent = remainMorning;
|
|
|
|
const remainMorningEl = document.getElementById('remainMorning');
|
|
if (remainMorning === 0) {
|
|
remainMorningEl.className = "font-bold text-lg leading-none text-gray-500";
|
|
} else if (remainMorning < 0) {
|
|
remainMorningEl.className = "font-bold text-lg leading-none text-red-600";
|
|
} else {
|
|
remainMorningEl.className = "font-bold text-lg leading-none text-blue-800";
|
|
}
|
|
|
|
document.getElementById('totalAfternoon').textContent = totalAfternoon;
|
|
document.getElementById('remainAfternoon').textContent = remainAfternoon;
|
|
|
|
const remainAfternoonEl = document.getElementById('remainAfternoon');
|
|
if (remainAfternoon === 0) {
|
|
remainAfternoonEl.className = "font-bold text-lg leading-none text-gray-500";
|
|
} else if (remainAfternoon < 0) {
|
|
remainAfternoonEl.className = "font-bold text-lg leading-none text-red-600";
|
|
} else {
|
|
remainAfternoonEl.className = "font-bold text-lg leading-none text-green-800";
|
|
}
|
|
}
|
|
|
|
const editPhoneInput = document.getElementById('editPhoneInput');
|
|
|
|
function openAddStaffModal() {
|
|
if (currentIsLocked) {
|
|
showLockAlert('เดือนนี้ถูกล็อกไว้ ไม่สามารถแก้ไขหรือเพิ่มบุคคลได้');
|
|
return;
|
|
}
|
|
|
|
currentEditingStaffId = 'new';
|
|
document.querySelector('#editNameModal h3').textContent = 'เพิ่มพนักงานใหม่';
|
|
editNameInput.value = '';
|
|
editPhoneInput.value = '';
|
|
// Reset preferences
|
|
document.querySelectorAll('input[name="pref_weekdays"]').forEach(cb => cb.checked = false);
|
|
document.querySelector('input[name="pref_weekend"][value=""]').checked = true;
|
|
document.getElementById('deleteStaffBtn').classList.add('hidden');
|
|
|
|
editNameModal.classList.remove('hidden');
|
|
editNameInput.focus();
|
|
}
|
|
|
|
function handleNameClick(e) {
|
|
if (currentIsLocked) {
|
|
showLockAlert('เดือนนี้ถูกล็อกไว้ ไม่สามารถแก้ไขหรือเพิ่มบุคคลได้');
|
|
return;
|
|
}
|
|
|
|
const cell = e.currentTarget;
|
|
currentEditingStaffId = parseInt(cell.dataset.staffId);
|
|
|
|
const staff = staffList.find(s => s.id === currentEditingStaffId);
|
|
if (staff) {
|
|
document.querySelector('#editNameModal h3').textContent = 'แก้ไขชื่อพนักงาน';
|
|
document.getElementById('deleteStaffBtn').classList.remove('hidden');
|
|
editNameInput.value = staff.name;
|
|
editPhoneInput.value = staff.phone || '';
|
|
|
|
// Populate preferences
|
|
const prefWeekdays = staff.pref_weekdays ? staff.pref_weekdays.split(',') : [];
|
|
document.querySelectorAll('input[name="pref_weekdays"]').forEach(cb => {
|
|
cb.checked = prefWeekdays.includes(cb.value);
|
|
});
|
|
|
|
const prefWeekend = staff.pref_weekend || '';
|
|
const weekendRadio = document.querySelector(`input[name="pref_weekend"][value="${prefWeekend}"]`);
|
|
if (weekendRadio) weekendRadio.checked = true;
|
|
|
|
editNameModal.classList.remove('hidden');
|
|
editNameInput.focus();
|
|
}
|
|
}
|
|
|
|
async function saveStaffName() {
|
|
if (currentEditingStaffId === null) return;
|
|
|
|
const newName = editNameInput.value.trim();
|
|
const newPhone = editPhoneInput.value.trim();
|
|
|
|
const selectedWeekdays = Array.from(document.querySelectorAll('input[name="pref_weekdays"]:checked')).map(cb => cb.value).join(',');
|
|
const selectedWeekend = document.querySelector('input[name="pref_weekend"]:checked').value;
|
|
|
|
if (newName) {
|
|
let shouldSave = false;
|
|
let payload = {
|
|
name: newName,
|
|
phone: newPhone,
|
|
pref_weekdays: selectedWeekdays,
|
|
pref_weekend: selectedWeekend
|
|
};
|
|
|
|
if (currentEditingStaffId === 'new') {
|
|
shouldSave = true;
|
|
} else {
|
|
const staffIndex = staffList.findIndex(s => s.id === currentEditingStaffId);
|
|
if (staffIndex !== -1 && (
|
|
staffList[staffIndex].name !== newName ||
|
|
staffList[staffIndex].phone !== newPhone ||
|
|
(staffList[staffIndex].pref_weekdays || '') !== selectedWeekdays ||
|
|
(staffList[staffIndex].pref_weekend || '') !== selectedWeekend
|
|
)) {
|
|
shouldSave = true;
|
|
payload.id = currentEditingStaffId;
|
|
}
|
|
}
|
|
|
|
if (shouldSave) {
|
|
// Show loading
|
|
saveNameModalText.classList.add('hidden');
|
|
saveNameModalSpinner.classList.remove('hidden');
|
|
saveNameModalBtn.disabled = true;
|
|
|
|
try {
|
|
const res = await fetch('api/staff.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
const result = await res.json();
|
|
|
|
if (result.status === 'success') {
|
|
if (currentEditingStaffId === 'new') {
|
|
staffList.push({
|
|
id: result.id,
|
|
name: newName,
|
|
phone: newPhone,
|
|
pref_weekdays: selectedWeekdays,
|
|
pref_weekend: selectedWeekend
|
|
});
|
|
} else {
|
|
const staffIndex = staffList.findIndex(s => s.id === currentEditingStaffId);
|
|
staffList[staffIndex].name = newName;
|
|
staffList[staffIndex].phone = newPhone;
|
|
staffList[staffIndex].pref_weekdays = selectedWeekdays;
|
|
staffList[staffIndex].pref_weekend = selectedWeekend;
|
|
}
|
|
renderSchedule();
|
|
renderContactAndOncall();
|
|
} else {
|
|
Swal.fire('ข้อผิดพลาด', 'เกิดข้อผิดพลาด: ' + result.message, 'error');
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
Swal.fire('ข้อผิดพลาด', 'เกิดข้อผิดพลาดในการเชื่อมต่อเซิร์ฟเวอร์', 'error');
|
|
} finally {
|
|
// Hide loading
|
|
saveNameModalText.classList.remove('hidden');
|
|
saveNameModalSpinner.classList.add('hidden');
|
|
saveNameModalBtn.disabled = false;
|
|
}
|
|
}
|
|
}
|
|
closeNameModal();
|
|
}
|
|
|
|
function closeNameModal() {
|
|
editNameModal.classList.add('hidden');
|
|
currentEditingStaffId = null;
|
|
}
|
|
|
|
async function deleteStaff() {
|
|
if (!currentEditingStaffId || currentEditingStaffId === 'new') return;
|
|
|
|
const swalResult = await Swal.fire({
|
|
title: 'ยืนยันการลบ',
|
|
text: 'คุณต้องการลบรายชื่อนี้ใช่หรือไม่? การกระทำนี้ไม่สามารถย้อนกลับได้ และข้อมูลการเข้าเวรของเจ้าหน้าที่คนนี้จะถูกลบไปด้วย',
|
|
icon: 'warning',
|
|
showCancelButton: true,
|
|
confirmButtonColor: '#d33',
|
|
cancelButtonColor: '#3085d6',
|
|
confirmButtonText: 'ลบ',
|
|
cancelButtonText: 'ยกเลิก'
|
|
});
|
|
|
|
if (swalResult.isConfirmed) {
|
|
try {
|
|
const res = await fetch('api/staff.php', {
|
|
method: 'DELETE',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ id: currentEditingStaffId })
|
|
});
|
|
const result = await res.json();
|
|
if (result.status === 'success') {
|
|
showToast('ลบรายชื่อเรียบร้อยแล้ว', 'success');
|
|
staffList = staffList.filter(s => s.id !== currentEditingStaffId);
|
|
delete scheduleData[currentEditingStaffId];
|
|
|
|
renderSchedule();
|
|
renderContactAndOncall();
|
|
closeNameModal();
|
|
} else {
|
|
Swal.fire('ข้อผิดพลาด', 'เกิดข้อผิดพลาดในการลบ: ' + result.message, 'error');
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
Swal.fire('ข้อผิดพลาด', 'เกิดข้อผิดพลาดในการเชื่อมต่อเซิร์ฟเวอร์', 'error');
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleCellClick(e) {
|
|
if (currentIsLocked) {
|
|
showLockAlert('เดือนนี้ถูกล็อกไว้ ไม่สามารถแก้ไขตารางเวรได้');
|
|
return;
|
|
}
|
|
|
|
const cell = e.currentTarget;
|
|
currentEditingCell = cell;
|
|
|
|
const staffId = cell.dataset.staffId;
|
|
const day = cell.dataset.day;
|
|
const isWeekendOrHoliday = cell.dataset.weekend === 'true';
|
|
|
|
const staffName = staffList.find(s => s.id == staffId).name;
|
|
const currentShift = scheduleData[staffId]?.[day] || '';
|
|
|
|
modalTitle.textContent = `กำหนดเวรให้ ${staffName}`;
|
|
modalSubtitle.textContent = `วันที่ ${day} ${monthYearDisplay.textContent}`;
|
|
|
|
if (!isWeekendOrHoliday) {
|
|
morningOption.classList.add('hidden');
|
|
bothOption.classList.add('hidden');
|
|
} else {
|
|
morningOption.classList.remove('hidden');
|
|
bothOption.classList.remove('hidden');
|
|
}
|
|
|
|
const radios = editModal.querySelectorAll('input[name="shiftType"]');
|
|
radios.forEach(radio => {
|
|
if (radio.value === currentShift) {
|
|
radio.checked = true;
|
|
} else if (currentShift === '' && radio.value === '') {
|
|
radio.checked = true;
|
|
}
|
|
});
|
|
|
|
editModal.classList.remove('hidden');
|
|
}
|
|
|
|
function getFullShiftName(shift) {
|
|
if (shift === 'ช') return 'เช้า';
|
|
if (shift === 'บ') return 'บ่าย';
|
|
if (shift === 'ช/บ') return 'เช้า/บ่าย';
|
|
return shift;
|
|
}
|
|
|
|
let currentConflicts = [];
|
|
|
|
function checkShiftConflict(targetStaffId, day, selectedShift) {
|
|
if (!selectedShift || selectedShift === 'ลา') return null;
|
|
|
|
let conflictingStaffs = [];
|
|
|
|
for (const sId in scheduleData) {
|
|
if (sId === targetStaffId) continue;
|
|
|
|
const existingShift = scheduleData[sId][day];
|
|
if (!existingShift || existingShift === 'ลา') continue;
|
|
|
|
// Check overlapping
|
|
const isConflict =
|
|
(selectedShift === 'ช' && (existingShift === 'ช' || existingShift === 'ช/บ')) ||
|
|
(selectedShift === 'บ' && (existingShift === 'บ' || existingShift === 'ช/บ')) ||
|
|
(selectedShift === 'ช/บ' && (existingShift === 'ช' || existingShift === 'บ' || existingShift === 'ช/บ'));
|
|
|
|
if (isConflict) {
|
|
const staffName = staffList.find(s => s.id == sId)?.name || 'พนักงาน';
|
|
conflictingStaffs.push({
|
|
id: sId,
|
|
text: `${staffName} (เวร${getFullShiftName(existingShift)})`
|
|
});
|
|
}
|
|
}
|
|
|
|
return conflictingStaffs.length > 0 ? conflictingStaffs : null;
|
|
}
|
|
|
|
function closeConflictModal() {
|
|
document.getElementById('conflictModal').classList.add('hidden');
|
|
}
|
|
|
|
async function forceSaveShift() {
|
|
window.bypassConflictCheck = true;
|
|
|
|
// Show loading on conflict modal
|
|
document.getElementById('confirmConflictText').classList.add('hidden');
|
|
document.getElementById('confirmConflictSpinner').classList.remove('hidden');
|
|
document.getElementById('confirmConflictBtn').disabled = true;
|
|
|
|
const day = currentEditingCell.dataset.day;
|
|
|
|
// Remove conflicting shifts first
|
|
for (const conflict of currentConflicts) {
|
|
try {
|
|
await fetch('api/schedules.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
staff_id: conflict.id,
|
|
year: currentYear,
|
|
month: currentMonth + 1,
|
|
day: day,
|
|
shift_type: '' // Clear
|
|
})
|
|
});
|
|
|
|
if (scheduleData[conflict.id]) {
|
|
delete scheduleData[conflict.id][day];
|
|
}
|
|
|
|
// Update UI for the conflicting staff
|
|
const conflictCell = document.querySelector(`td[data-staff-id="${conflict.id}"][data-day="${day}"]`);
|
|
if (conflictCell) conflictCell.innerHTML = '';
|
|
|
|
const countEl = document.getElementById(`count-${conflict.id}`);
|
|
if (countEl) countEl.textContent = calculateShiftCount(conflict.id);
|
|
|
|
} catch (e) {
|
|
console.error('Failed to clear conflicting shift:', e);
|
|
}
|
|
}
|
|
|
|
// Now save the new shift
|
|
saveShift().then(() => {
|
|
closeConflictModal();
|
|
// Reset loading
|
|
document.getElementById('confirmConflictText').classList.remove('hidden');
|
|
document.getElementById('confirmConflictSpinner').classList.add('hidden');
|
|
document.getElementById('confirmConflictBtn').disabled = false;
|
|
window.bypassConflictCheck = false;
|
|
});
|
|
}
|
|
|
|
async function saveShift() {
|
|
if (!currentEditingCell) return;
|
|
|
|
const staffId = currentEditingCell.dataset.staffId;
|
|
const day = currentEditingCell.dataset.day;
|
|
const selectedShift = editModal.querySelector('input[name="shiftType"]:checked').value;
|
|
|
|
// ตรวจสอบความซ้ำซ้อน
|
|
if (!window.bypassConflictCheck) {
|
|
const conflict = checkShiftConflict(staffId, day, selectedShift);
|
|
if (conflict) {
|
|
currentConflicts = conflict;
|
|
const listEl = document.getElementById('conflictStaffList');
|
|
listEl.innerHTML = conflict.map(c => `<div>• ${c.text}</div>`).join('');
|
|
document.getElementById('conflictModal').classList.remove('hidden');
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Show loading
|
|
saveModalText.classList.add('hidden');
|
|
saveModalSpinner.classList.remove('hidden');
|
|
saveModalBtn.disabled = true;
|
|
|
|
try {
|
|
const res = await fetch('api/schedules.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
staff_id: staffId,
|
|
year: currentYear,
|
|
month: currentMonth + 1,
|
|
day: day,
|
|
shift_type: selectedShift
|
|
})
|
|
});
|
|
|
|
const result = await res.json();
|
|
|
|
if (result.status === 'success') {
|
|
// Update local data
|
|
if (!scheduleData[staffId]) {
|
|
scheduleData[staffId] = {};
|
|
}
|
|
|
|
if (selectedShift === '') {
|
|
delete scheduleData[staffId][day];
|
|
} else {
|
|
scheduleData[staffId][day] = selectedShift;
|
|
}
|
|
|
|
// Update UI optimistically
|
|
let shiftClass = 'inline-block align-middle leading-none mt-1';
|
|
if (selectedShift === 'ช') shiftClass += ' shift-m';
|
|
else if (selectedShift === 'บ') shiftClass += ' shift-a';
|
|
else if (selectedShift === 'ช/บ') shiftClass += ' shift-ma text-xs';
|
|
else if (selectedShift === 'ลา') shiftClass += ' shift-l text-xs';
|
|
|
|
currentEditingCell.innerHTML = `<span class="${shiftClass}">${selectedShift}</span>`;
|
|
|
|
const newCount = calculateShiftCount(staffId);
|
|
document.getElementById(`count-${staffId}`).textContent = newCount;
|
|
|
|
closeModal();
|
|
} else {
|
|
Swal.fire('ข้อผิดพลาด', 'เกิดข้อผิดพลาด: ' + result.message, 'error');
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
Swal.fire('ข้อผิดพลาด', 'เกิดข้อผิดพลาดในการเชื่อมต่อเซิร์ฟเวอร์', 'error');
|
|
} finally {
|
|
// Restore button
|
|
saveModalText.classList.remove('hidden');
|
|
saveModalSpinner.classList.add('hidden');
|
|
saveModalBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
function closeModal() {
|
|
editModal.classList.add('hidden');
|
|
currentEditingCell = null;
|
|
}
|
|
|
|
updateMonthBtn.addEventListener('click', async () => {
|
|
currentMonth = parseInt(monthSelect.value);
|
|
const y = parseInt(yearSelect.value);
|
|
if (y > 2500) {
|
|
currentYear = y - 543;
|
|
} else {
|
|
currentYear = y;
|
|
}
|
|
|
|
showLoading();
|
|
await fetchHolidays(currentYear);
|
|
await fetchSchedules(currentYear, currentMonth + 1);
|
|
hideLoading();
|
|
renderSchedule();
|
|
});
|
|
|
|
// Image Export Logic
|
|
exportImageBtn.addEventListener('click', async () => {
|
|
const captureArea = document.getElementById('captureArea');
|
|
const exportHeader = document.getElementById('exportHeader');
|
|
const exportMonthYear = document.getElementById('exportMonthYear');
|
|
|
|
exportMonthYear.textContent = monthYearDisplay.textContent;
|
|
exportHeader.classList.remove('hidden');
|
|
exportHeader.classList.add('flex');
|
|
captureArea.classList.add('exporting');
|
|
|
|
const stickyElements = captureArea.querySelectorAll('.sticky');
|
|
stickyElements.forEach(el => el.classList.remove('sticky'));
|
|
|
|
try {
|
|
const originalText = exportImageBtn.innerHTML;
|
|
exportImageBtn.innerHTML = 'กำลังประมวลผล...';
|
|
exportImageBtn.disabled = true;
|
|
|
|
const canvas = await html2canvas(captureArea, {
|
|
scale: 2,
|
|
backgroundColor: '#ffffff',
|
|
logging: false,
|
|
useCORS: true
|
|
});
|
|
|
|
const image = canvas.toDataURL('image/png');
|
|
const link = document.createElement('a');
|
|
link.href = image;
|
|
link.download = `ตารางเวร_${monthYearDisplay.textContent.replace(/\s+/g, '_')}.png`;
|
|
link.click();
|
|
|
|
} catch (error) {
|
|
console.error('Error exporting image:', error);
|
|
Swal.fire('ข้อผิดพลาด', 'เกิดข้อผิดพลาดในการสร้างรูปภาพ', 'error');
|
|
} finally {
|
|
exportHeader.classList.add('hidden');
|
|
exportHeader.classList.remove('flex');
|
|
captureArea.classList.remove('exporting');
|
|
stickyElements.forEach(el => el.classList.add('sticky'));
|
|
|
|
exportImageBtn.innerHTML = `
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
|
</svg>
|
|
ส่งออกเป็นรูปภาพ
|
|
`;
|
|
exportImageBtn.disabled = false;
|
|
}
|
|
});
|
|
|
|
// Event Listeners for Shift Modal
|
|
cancelModalBtn.addEventListener('click', closeModal);
|
|
saveModalBtn.addEventListener('click', saveShift);
|
|
|
|
// Event Listeners for Name Modal
|
|
cancelNameModalBtn.addEventListener('click', closeNameModal);
|
|
saveNameModalBtn.addEventListener('click', saveStaffName);
|
|
|
|
editNameInput.addEventListener('keypress', function (e) {
|
|
if (e.key === 'Enter') {
|
|
saveStaffName();
|
|
}
|
|
});
|
|
|
|
editModal.addEventListener('click', (e) => {
|
|
if (e.target === editModal) {
|
|
closeModal();
|
|
}
|
|
});
|
|
|
|
editNameModal.addEventListener('click', (e) => {
|
|
if (e.target === editNameModal) {
|
|
closeNameModal();
|
|
}
|
|
});
|
|
|
|
async function autoGenerateSchedule() {
|
|
if (currentIsLocked) {
|
|
showLockAlert('เดือนนี้ถูกล็อกไว้ ไม่สามารถจัดเวรอัตโนมัติได้');
|
|
return;
|
|
}
|
|
|
|
// Check if month already has data
|
|
const hasData = Object.keys(scheduleData).some(staffId => Object.keys(scheduleData[staffId]).length > 0);
|
|
|
|
if (hasData) {
|
|
const swalResult = await Swal.fire({
|
|
title: '⚠️ แจ้งเตือน ⚠️',
|
|
text: `เดือน ${monthYearDisplay.textContent} มีการจัดเวรไว้แล้ว\nหากกดยืนยัน ระบบจะลบข้อมูลเดิมของเดือนนี้ทั้งหมดและจัดตารางใหม่\n\nต้องการจัดตารางใหม่หรือไม่?`,
|
|
icon: 'warning',
|
|
showCancelButton: true,
|
|
confirmButtonColor: '#d33',
|
|
cancelButtonColor: '#3085d6',
|
|
confirmButtonText: 'ยืนยัน',
|
|
cancelButtonText: 'ยกเลิก'
|
|
});
|
|
if (!swalResult.isConfirmed) return;
|
|
} else {
|
|
const swalResult = await Swal.fire({
|
|
title: 'ยืนยัน',
|
|
text: `ต้องการให้ระบบจัดตารางเวรอัตโนมัติสำหรับเดือน ${monthYearDisplay.textContent} หรือไม่?`,
|
|
icon: 'question',
|
|
showCancelButton: true,
|
|
confirmButtonColor: '#3085d6',
|
|
cancelButtonColor: '#d33',
|
|
confirmButtonText: 'ใช่',
|
|
cancelButtonText: 'ยกเลิก'
|
|
});
|
|
if (!swalResult.isConfirmed) return;
|
|
}
|
|
|
|
showLoading();
|
|
|
|
try {
|
|
const daysInMonth = getDaysInMonth(currentMonth, currentYear);
|
|
let newSchedules = [];
|
|
|
|
for (let day = 1; day <= daysInMonth; day++) {
|
|
const isWknd = isWeekend(day, currentMonth, currentYear);
|
|
const isHol = isHoliday(day, currentMonth, currentYear);
|
|
const isOffDay = isWknd || isHol;
|
|
|
|
const dateObj = new Date(currentYear, currentMonth, day);
|
|
let dayOfWeek = dateObj.getDay(); // 0:Sun, 1:Mon, ..., 6:Sat
|
|
if (dayOfWeek === 0) dayOfWeek = 7; // Map Sun to 7, though standard pref expects 1-5 for Mon-Fri
|
|
|
|
// Shuffle staff list to distribute fairly if multiple people prefer the same day
|
|
const shuffledStaff = [...staffList].sort(() => 0.5 - Math.random());
|
|
|
|
if (!isOffDay) {
|
|
// Weekday: Look for staff who preferred this day (dayOfWeek 1-5)
|
|
for (const staff of shuffledStaff) {
|
|
if (staff.pref_weekdays) {
|
|
const prefs = staff.pref_weekdays.split(',');
|
|
if (prefs.includes(dayOfWeek.toString())) {
|
|
newSchedules.push({
|
|
staff_id: staff.id,
|
|
day: day,
|
|
shift_type: 'บ' // Default afternoon for weekdays
|
|
});
|
|
break; // Only 1 person per shift type usually, but we can just stop at the first match for simplicity
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Weekend/Holiday
|
|
let morningFilled = false;
|
|
let afternoonFilled = false;
|
|
|
|
for (const staff of shuffledStaff) {
|
|
if (!staff.pref_weekend) continue;
|
|
|
|
if (staff.pref_weekend === 'ช' && !morningFilled) {
|
|
newSchedules.push({ staff_id: staff.id, day: day, shift_type: 'ช' });
|
|
morningFilled = true;
|
|
} else if (staff.pref_weekend === 'บ' && !afternoonFilled) {
|
|
newSchedules.push({ staff_id: staff.id, day: day, shift_type: 'บ' });
|
|
afternoonFilled = true;
|
|
} else if (staff.pref_weekend === 'ช/บ' && !morningFilled && !afternoonFilled) {
|
|
newSchedules.push({ staff_id: staff.id, day: day, shift_type: 'ช/บ' });
|
|
morningFilled = true;
|
|
afternoonFilled = true;
|
|
}
|
|
|
|
if (morningFilled && afternoonFilled) break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Send bulk update to API
|
|
const response = await fetch('api/schedules.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
bulk: true,
|
|
clear_month: true, // we wipe out previous schedules for the month
|
|
year: currentYear,
|
|
month: currentMonth + 1,
|
|
schedules: newSchedules
|
|
})
|
|
});
|
|
|
|
const result = await response.json();
|
|
if (result.status === 'success') {
|
|
// Refetch and re-render
|
|
await fetchSchedules(currentYear, currentMonth + 1);
|
|
renderSchedule();
|
|
Swal.fire('สำเร็จ', 'จัดเวรอัตโนมัติเสร็จสมบูรณ์!', 'success');
|
|
} else {
|
|
Swal.fire('ข้อผิดพลาด', 'เกิดข้อผิดพลาดในการจัดเวร: ' + result.message, 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
Swal.fire('ข้อผิดพลาด', 'เกิดข้อผิดพลาดในการเชื่อมต่อเซิร์ฟเวอร์', 'error');
|
|
} finally {
|
|
hideLoading();
|
|
}
|
|
}
|
|
|
|
|
|
// Start App
|
|
initApp();
|
|
function openDocuments() {
|
|
window.open(`documents.php?year=${currentYear}&month=${currentMonth + 1}`, '_blank');
|
|
}
|
|
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|