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,97 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0); // Prevent PHP errors from breaking JSON format
header('Content-Type: application/json');
try {
require_once '../config.php';
if (!isLoggedIn()) {
echo json_encode(['data' => []]);
exit;
}
$user_id = $_SESSION['user_id'];
$role = $_SESSION['role'] ?? 'student';
$filter = $_GET['filter'] ?? 'urgent';
$days_before = getSetting($pdo, 'days_before_due') ?: 3;
$sql = "SELECT a.id, a.title, a.due_date, a.status, a.created_at, u.name as student_name, DATEDIFF(a.due_date, CURDATE()) as days_left
FROM assignments a
LEFT JOIN users u ON a.student_id = u.id
WHERE 1=1";
$params = [];
if ($role !== 'admin') {
$sql .= " AND a.student_id = :student_id";
$params[':student_id'] = $user_id;
}
if ($filter === 'urgent') {
$sql .= " AND a.status = 'pending' AND DATEDIFF(a.due_date, CURDATE()) <= :days_before AND DATEDIFF(a.due_date, CURDATE()) >= 0";
$params[':days_before'] = $days_before;
} elseif ($filter === 'pending') {
$sql .= " AND a.status = 'pending'";
} elseif ($filter === 'completed') {
$sql .= " AND a.status = 'completed'";
}
// "งานที่ถูกเพิ่มล่าสุดอยู่บนสุด"
$sql .= " ORDER BY a.id DESC";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$assignments = $stmt->fetchAll();
$data = [];
foreach ($assignments as $index => $item) {
$status_html = '';
if ($item['status'] === 'pending') {
$status_html = '<span class="bg-yellow-100 text-yellow-800 py-1 px-2 rounded-full text-xs font-semibold">รอดำเนินการ</span>';
} else {
$status_html = '<span class="bg-green-100 text-green-800 py-1 px-2 rounded-full text-xs font-semibold">เสร็จสิ้น</span>';
}
// Date formatting: วัน-เดือน-ปี (พ.ศ.)
$due_timestamp = strtotime($item['due_date']);
$due_date_th = date('d-m-', $due_timestamp) . (date('Y', $due_timestamp) + 543);
$created_timestamp = strtotime($item['created_at']);
$created_date_th = date('d-m-', $created_timestamp) . (date('Y', $created_timestamp) + 543);
$due_html = $due_date_th;
if ($item['status'] === 'pending') {
if ($item['days_left'] < 0) {
// เลยกำหนด สีดำ
$due_html = "<span>{$due_date_th}</span> <span class=\"bg-gray-800 text-white text-xs px-2 py-0.5 rounded ml-2 whitespace-nowrap\">เลยกำหนด " . abs($item['days_left']) . " วัน</span>";
} elseif ($item['days_left'] <= $days_before) {
// อยู่ในช่วงเตือน สีส้ม
$due_html = "<span>{$due_date_th}</span> <span class=\"bg-orange-500 text-white text-xs px-2 py-0.5 rounded ml-2 whitespace-nowrap\">เหลือ {$item['days_left']} วัน</span>";
} else {
// ปกติ
$due_html = "<span>{$due_date_th}</span> <span class=\"bg-gray-100 text-gray-600 text-xs px-2 py-0.5 rounded ml-2 whitespace-nowrap\">เหลือ {$item['days_left']} วัน</span>";
}
}
$row = [
'index' => $index + 1,
'title' => htmlspecialchars($item['title'] ?? ''),
'created_at' => $created_date_th,
'due_date' => $due_html,
'status' => $status_html
];
if ($role === 'admin') {
$row['student'] = htmlspecialchars($item['student_name'] ?? 'ไม่ระบุ');
}
$data[] = $row;
}
echo json_encode(['data' => $data]);
} catch (Exception $e) {
// Return error as JSON so DataTables can at least parse it, or we can see it in network tab
echo json_encode(['error' => $e->getMessage(), 'data' => []]);
}
@@ -0,0 +1,63 @@
<?php
require_once '../config.php';
header('Content-Type: application/json');
if (!isLoggedIn() || !isAdmin()) {
echo json_encode(['data' => []]);
exit;
}
try {
$sql = "SELECT l.id, l.action, l.details, l.ip_address, l.created_at, u.name as user_name, u.role as user_role
FROM activity_logs l
LEFT JOIN users u ON l.user_id = u.id
ORDER BY l.created_at DESC LIMIT 1000"; // limit to prevent huge payloads
$stmt = $pdo->prepare($sql);
$stmt->execute();
$logs = $stmt->fetchAll();
$data = [];
foreach ($logs as $log) {
$actor = '';
if ($log['user_name']) {
$badge = $log['user_role'] === 'admin' ? '<span class="bg-purple-100 text-purple-800 px-2 py-0.5 rounded text-xs ml-1">Admin</span>' : '';
$actor = '<div><strong>' . htmlspecialchars($log['user_name']) . '</strong>' . $badge . '</div>';
} else {
// System or failed login
if (strpos($log['action'], 'CRON') !== false) {
$actor = '<span class="text-gray-500"><i class="fas fa-robot mr-1"></i> System (Cron)</span>';
} else {
$actor = '<span class="text-gray-500"><i class="fas fa-user-secret mr-1"></i> Guest / System</span>';
}
}
// Format action nicely
$action_html = htmlspecialchars($log['action']);
if (strpos($log['action'], 'LOGIN') !== false) {
$action_html = '<span class="text-blue-600 font-semibold"><i class="fas fa-sign-in-alt mr-1"></i> ' . $action_html . '</span>';
} elseif (strpos($log['action'], 'CREATE') !== false) {
$action_html = '<span class="text-green-600 font-semibold"><i class="fas fa-plus-circle mr-1"></i> ' . $action_html . '</span>';
} elseif (strpos($log['action'], 'UPDATE') !== false) {
$action_html = '<span class="text-orange-600 font-semibold"><i class="fas fa-edit mr-1"></i> ' . $action_html . '</span>';
} elseif (strpos($log['action'], 'DELETE') !== false) {
$action_html = '<span class="text-red-600 font-semibold"><i class="fas fa-trash-alt mr-1"></i> ' . $action_html . '</span>';
} elseif (strpos($log['action'], 'CRON') !== false) {
$action_html = '<span class="text-indigo-600 font-semibold"><i class="fas fa-paper-plane mr-1"></i> ' . $action_html . '</span>';
}
$data[] = [
'id' => $log['id'],
'created_at' => date('d/m/Y H:i:s', strtotime($log['created_at'])),
'user' => $actor,
'action' => $action_html,
'details' => '<div class="text-xs text-gray-600 break-words max-w-md">' . htmlspecialchars($log['details']) . '</div>',
'ip_address' => '<span class="text-xs text-gray-500 font-mono">' . htmlspecialchars($log['ip_address']) . '</span>'
];
}
echo json_encode(['data' => $data]);
} catch (Exception $e) {
echo json_encode(['error' => $e->getMessage(), 'data' => []]);
}
?>
@@ -0,0 +1,37 @@
<?php
require_once '../config.php';
header('Content-Type: application/json');
if (!isLoggedIn()) {
echo json_encode(['status' => 'error', 'message' => 'Unauthorized']);
exit;
}
$user_id = $_SESSION['user_id'];
$role = $_SESSION['role'];
// Get days before due setting
$days_before_due = getSetting($pdo, 'days_before_due') ?: 3;
$sql = "SELECT id, title, due_date, DATEDIFF(due_date, CURDATE()) as days_left
FROM assignments
WHERE status = 'pending'
AND DATEDIFF(due_date, CURDATE()) <= :days_before_due
AND DATEDIFF(due_date, CURDATE()) >= 0";
$params = [':days_before_due' => $days_before_due];
if ($role !== 'admin') {
$sql .= " AND student_id = :student_id";
$params[':student_id'] = $user_id;
}
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$assignments = $stmt->fetchAll();
echo json_encode([
'status' => 'success',
'data' => $assignments
]);
?>
@@ -0,0 +1,107 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0); // Don't print errors to output directly to not break JSON
require_once '../config.php';
header('Content-Type: application/json');
if (!function_exists('curl_init')) {
echo json_encode(['status' => 'error', 'message' => 'เซิร์ฟเวอร์ของคุณไม่รองรับ cURL (cURL extension is missing)']);
exit;
}
if (!isLoggedIn() || !isAdmin()) {
echo json_encode(['status' => 'error', 'message' => 'Unauthorized access']);
exit;
}
$type = $_POST['type'] ?? '';
if ($type === 'line') {
$token = $_POST['token'] ?? '';
if (empty($token)) {
echo json_encode(['status' => 'error', 'message' => 'กรุณากรอก Token ก่อนทดสอบ']);
exit;
}
$tomorrow = date('d/m/Y', strtotime('+1 day'));
$message = "⚠️ แจ้งเตือนกำหนดส่งงาน ⚠️\n";
$message .= "เรียนคุณ: นักเรียนทดสอบ (Test)\n";
$message .= "งาน: ทดสอบระบบแจ้งเตือน\n";
$message .= "กำหนดส่ง: {$tomorrow}\n";
$message .= "สถานะ: เหลือเวลาอีก 1 วัน!\n";
$message .= "กรุณาตรวจสอบระบบ";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://notify-api.line.me/api/notify");
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query(array('message' => $message)));
$headers = array('Content-type: application/x-www-form-urlencoded', 'Authorization: Bearer ' . $token);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result = curl_exec($ch);
$error_msg = curl_error($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($result === false) {
echo json_encode(['status' => 'error', 'message' => 'cURL Error: ' . $error_msg]);
} elseif ($httpCode == 200) {
echo json_encode(['status' => 'success', 'message' => 'ส่งข้อความทดสอบ LINE สำเร็จ']);
} else {
$resObj = json_decode($result, true);
$reason = isset($resObj['message']) ? $resObj['message'] : 'HTTP Code ' . $httpCode;
echo json_encode(['status' => 'error', 'message' => 'ไม่สามารถส่งข้อความได้: ' . $reason]);
}
} elseif ($type === 'telegram') {
$bot_token = $_POST['bot_token'] ?? '';
$chat_id = $_POST['chat_id'] ?? '';
if (empty($bot_token) || empty($chat_id)) {
echo json_encode(['status' => 'error', 'message' => 'กรุณากรอก Bot Token และ Chat ID ก่อนทดสอบ']);
exit;
}
$tomorrow = date('d/m/Y', strtotime('+1 day'));
$message = "⚠️ <b>แจ้งเตือนกำหนดส่งงาน (Test)</b> ⚠️\n\n";
$message .= "👤 เรียนคุณ: นักเรียนทดสอบ\n";
$message .= "📝 งาน: <b>ทดสอบระบบการแจ้งเตือน</b>\n";
$message .= "📅 กำหนดส่ง: {$tomorrow}\n";
$message .= "⏳ สถานะ: <b>เหลือเวลาอีก 1 วัน!</b>\n";
$url = "https://api.telegram.org/bot{$bot_token}/sendMessage";
$data = [
'chat_id' => $chat_id,
'text' => $message,
'parse_mode' => 'HTML'
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($ch);
$error_msg = curl_error($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($result === false) {
echo json_encode(['status' => 'error', 'message' => 'cURL Error: ' . $error_msg]);
} elseif ($httpCode == 200) {
echo json_encode(['status' => 'success', 'message' => 'ส่งข้อความทดสอบ Telegram สำเร็จ']);
} else {
$resObj = json_decode($result, true);
$reason = isset($resObj['description']) ? $resObj['description'] : 'HTTP Code ' . $httpCode;
echo json_encode(['status' => 'error', 'message' => 'ไม่สามารถส่งข้อความได้: ' . $reason]);
}
} else {
echo json_encode(['status' => 'error', 'message' => 'Invalid request type']);
}
?>
Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

@@ -0,0 +1,15 @@
document.addEventListener('DOMContentLoaded', function() {
const mobileMenuButton = document.getElementById('mobile-menu-button');
const sidebar = document.querySelector('.bg-gray-800.text-white.w-64');
if (mobileMenuButton && sidebar) {
mobileMenuButton.addEventListener('click', function() {
sidebar.classList.toggle('hidden');
if (!sidebar.classList.contains('hidden')) {
sidebar.classList.add('absolute', 'z-50', 'h-full');
} else {
sidebar.classList.remove('absolute', 'z-50', 'h-full');
}
});
}
});
@@ -0,0 +1,387 @@
<?php
require_once 'config.php';
requireLogin();
requireAdmin();
$action = $_GET['action'] ?? 'list';
$error = '';
$success = '';
// Handle form submissions
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$title = $_POST['title'] ?? '';
$description = $_POST['description'] ?? '';
$due_date = $_POST['due_date'] ?? '';
$student_id = $_POST['student_id'] ?? '';
if (isset($_POST['add'])) {
$student_ids = $_POST['student_ids'] ?? [];
if (empty($title) || empty($due_date) || empty($student_ids)) {
$error = 'กรุณากรอกข้อมูลให้ครบถ้วน (เลือกนักเรียนอย่างน้อย 1 คน)';
} else {
$stmt = $pdo->prepare("INSERT INTO assignments (title, description, due_date, student_id) VALUES (?, ?, ?, ?)");
$success_count = 0;
foreach ($student_ids as $sid) {
if ($stmt->execute([$title, $description, $due_date, $sid])) {
$new_id = $pdo->lastInsertId();
logActivity($pdo, 'CREATE_ASSIGNMENT', "Created assignment ID: $new_id, Title: $title, Student ID: $sid");
$success_count++;
}
}
if ($success_count > 0) {
$success = "เพิ่มงานสำเร็จให้แก่ $success_count คน";
$action = 'list';
} else {
$error = 'เกิดข้อผิดพลาดในการเพิ่มงาน';
}
}
} elseif (isset($_POST['edit'])) {
$id = $_POST['id'] ?? '';
$status = $_POST['status'] ?? 'pending';
$student_ids = $_POST['student_ids'] ?? [];
if (empty($title) || empty($due_date) || empty($student_ids)) {
$error = 'กรุณากรอกข้อมูลให้ครบถ้วน (เลือกนักเรียนอย่างน้อย 1 คน)';
} else {
// Update the current assignment with the FIRST selected student
$first_student_id = array_shift($student_ids);
$stmt = $pdo->prepare("UPDATE assignments SET title=?, description=?, due_date=?, student_id=?, status=? WHERE id=?");
if ($stmt->execute([$title, $description, $due_date, $first_student_id, $status, $id])) {
logActivity($pdo, 'UPDATE_ASSIGNMENT', "Updated assignment ID: $id, Title: $title, Status: $status");
$success = 'แก้ไขงานสำเร็จ';
// If more students were checked, insert them as new assignments (cloning)
if (!empty($student_ids)) {
$insert_stmt = $pdo->prepare("INSERT INTO assignments (title, description, due_date, student_id, status) VALUES (?, ?, ?, ?, ?)");
$added_count = 0;
foreach ($student_ids as $sid) {
if ($insert_stmt->execute([$title, $description, $due_date, $sid, $status])) {
$added_count++;
}
}
if ($added_count > 0) {
$success .= " และเพิ่มงานใหม่ให้กับนักเรียนที่เลือกเพิ่มอีก $added_count คน";
}
}
$action = 'list';
} else {
$error = 'เกิดข้อผิดพลาดในการแก้ไขงาน';
}
}
}
}
// Handle delete
if (isset($_GET['delete'])) {
$id = $_GET['delete'];
// Get assignment info before deleting for log
$stmt_info = $pdo->prepare("SELECT title FROM assignments WHERE id=?");
$stmt_info->execute([$id]);
$assign_info = $stmt_info->fetch();
$stmt = $pdo->prepare("DELETE FROM assignments WHERE id=?");
if ($stmt->execute([$id])) {
$title = $assign_info ? $assign_info['title'] : 'Unknown';
logActivity($pdo, 'DELETE_ASSIGNMENT', "Deleted assignment ID: $id, Title: $title");
$success = 'ลบงานสำเร็จ';
}
}
// Fetch students for dropdowns
$stmt = $pdo->query("SELECT id, name FROM users WHERE role = 'student' ORDER BY name");
$students = $stmt->fetchAll();
include 'includes/header.php';
include 'includes/sidebar.php';
?>
<div class="mb-6 flex justify-between items-center">
<div>
<h2 class="text-2xl font-bold text-gray-800">จัดการงาน</h2>
<p class="text-gray-600 mt-1">เพิ่ม แก้ไข และลบงานที่มอบหมายให้นักเรียน</p>
</div>
<?php if ($action === 'list'): ?>
<a href="?action=add" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded shadow">
<i class="fas fa-plus mr-2"></i> เพิ่มงานใหม่
</a>
<?php else: ?>
<a href="assignments.php" class="bg-gray-500 hover:bg-gray-600 text-white font-bold py-2 px-4 rounded shadow">
<i class="fas fa-arrow-left mr-2"></i> กลับ
</a>
<?php endif; ?>
</div>
<?php if ($error): ?>
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-6 rounded shadow-sm">
<p><i class="fas fa-exclamation-circle mr-2"></i> <?= htmlspecialchars($error) ?></p>
</div>
<?php endif; ?>
<?php if ($success): ?>
<div class="bg-green-100 border-l-4 border-green-500 text-green-700 p-4 mb-6 rounded shadow-sm">
<p><i class="fas fa-check-circle mr-2"></i> <?= htmlspecialchars($success) ?></p>
</div>
<script>
Swal.fire({
icon: 'success',
title: 'สำเร็จ',
text: '<?= addslashes($success) ?>',
timer: 2000,
showConfirmButton: false
});
</script>
<?php endif; ?>
<?php if ($action === 'list'): ?>
<div class="bg-white rounded-lg shadow-md p-6">
<table class="dataTable display w-full text-sm text-left text-gray-500">
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
<th class="text-center w-16">ลำดับ</th>
<th class="text-center">ชื่องาน/ชื่อวิชา</th>
<th class="text-center">นักเรียน</th>
<th class="text-center w-32">วันที่เพิ่มข้อมูล</th>
<th class="text-center">กำหนดส่ง</th>
<th class="text-center w-24">สถานะ</th>
<th class="text-center">จัดการ</th>
</tr>
</thead>
<tbody>
<?php
$days_before = getSetting($pdo, 'days_before_due') ?: 3;
$stmt = $pdo->query("SELECT a.*, u.name as student_name, DATEDIFF(a.due_date, CURDATE()) as days_left FROM assignments a LEFT JOIN users u ON a.student_id = u.id ORDER BY a.id DESC");
$index = 1;
while ($row = $stmt->fetch()):
// Get all assigned students for this specific task
$stmt_group = $pdo->prepare("SELECT u.name FROM assignments a JOIN users u ON a.student_id = u.id WHERE a.title = ? AND a.due_date = ?");
$stmt_group->execute([$row['title'], $row['due_date']]);
$assigned_names = $stmt_group->fetchAll(PDO::FETCH_COLUMN);
$assigned_list = implode(', ', $assigned_names);
// Format Created At
$created_timestamp = strtotime($row['created_at']);
$created_date_th = date('d-m-', $created_timestamp) . (date('Y', $created_timestamp) + 543);
// Format Due Date
$due_timestamp = strtotime($row['due_date']);
$due_date_th = date('d-m-', $due_timestamp) . (date('Y', $due_timestamp) + 543);
$due_html = "<span>{$due_date_th}</span>";
$status_text = "รอดำเนินการ";
if ($row['status'] === 'pending') {
if ($row['days_left'] < 0) {
$due_html .= " <span class=\"bg-gray-800 text-white text-xs px-2 py-0.5 rounded ml-2 whitespace-nowrap\">เลยกำหนด " . abs($row['days_left']) . " วัน</span>";
} elseif ($row['days_left'] <= $days_before) {
$due_html .= " <span class=\"bg-orange-500 text-white text-xs px-2 py-0.5 rounded ml-2 whitespace-nowrap\">เหลือ {$row['days_left']} วัน</span>";
} else {
$due_html .= " <span class=\"bg-gray-100 text-gray-600 text-xs px-2 py-0.5 rounded ml-2 whitespace-nowrap\">เหลือ {$row['days_left']} วัน</span>";
}
} else {
$status_text = "เสร็จสิ้น";
}
?>
<tr class="bg-white border-b hover:bg-gray-50">
<td class="text-center"><?= $index++ ?></td>
<td class="font-medium text-gray-900"><?= htmlspecialchars($row['title']) ?></td>
<td><?= htmlspecialchars($row['student_name']) ?></td>
<td class="text-center"><?= $created_date_th ?></td>
<td class="text-center"><?= $due_html ?></td>
<td class="text-center">
<?php if ($row['status'] === 'submitted' || $row['status'] === 'completed'): ?>
<span class="bg-green-100 text-green-800 py-1 px-2 rounded-full text-xs font-semibold">เสร็จสิ้น</span>
<?php else: ?>
<span class="bg-yellow-100 text-yellow-800 py-1 px-2 rounded-full text-xs font-semibold">รอดำเนินการ</span>
<?php endif; ?>
</td>
<td class="text-center whitespace-nowrap">
<button type="button"
onclick="showDetails(
'<?= htmlspecialchars(addslashes($row['title'])) ?>',
'<?= htmlspecialchars(addslashes($row['description'] ?: '-')) ?>',
'<?= htmlspecialchars(addslashes($assigned_list)) ?>',
'<?= $due_date_th ?>',
'<?= $status_text ?>'
)"
class="text-white bg-blue-500 hover:bg-blue-600 font-medium rounded-lg text-sm px-3 py-1 mr-1" title="ดูรายละเอียด"><i class="fas fa-eye"></i></button>
<a href="?action=edit&id=<?= $row['id'] ?>" class="text-white bg-yellow-400 hover:bg-yellow-500 font-medium rounded-lg text-sm px-3 py-1 mr-1" title="แก้ไข"><i class="fas fa-edit"></i></a>
<button onclick="confirmDelete(<?= $row['id'] ?>)" class="text-white bg-red-600 hover:bg-red-700 font-medium rounded-lg text-sm px-3 py-1" title="ลบ"><i class="fas fa-trash"></i></button>
</td>
</tr>
<?php endwhile; ?>
</tbody>
</table>
</div>
<script>
function showDetails(title, desc, assignedTo, dueDate, status) {
const statusColor = status === 'เสร็จสิ้น' ? 'text-green-600' : 'text-yellow-600';
let htmlContent = `
<div class="text-left space-y-4 text-sm mt-4">
<div class="bg-gray-50 p-3 rounded border">
<p class="text-gray-500 mb-1"><i class="fas fa-align-left mr-2"></i><strong>รายละเอียดงาน:</strong></p>
<p class="text-gray-800 whitespace-pre-wrap">${desc}</p>
</div>
<div class="flex items-center justify-between border-b pb-2">
<span class="text-gray-500"><i class="fas fa-users mr-2"></i><strong>ผู้ที่ถูกมอบหมาย:</strong></span>
<span class="text-gray-800 text-right ml-4">${assignedTo}</span>
</div>
<div class="flex items-center justify-between border-b pb-2">
<span class="text-gray-500"><i class="far fa-calendar-alt mr-2"></i><strong>วันที่กำหนดส่ง:</strong></span>
<span class="text-gray-800 font-medium">${dueDate}</span>
</div>
<div class="flex items-center justify-between pb-2">
<span class="text-gray-500"><i class="fas fa-info-circle mr-2"></i><strong>สถานะปัจจุบัน:</strong></span>
<span class="font-bold ${statusColor}">${status}</span>
</div>
</div>
`;
Swal.fire({
title: `<div class="text-xl text-blue-700 font-bold border-b pb-3">${title}</div>`,
html: htmlContent,
width: 600,
showCloseButton: true,
confirmButtonText: 'ปิดหน้าต่าง',
confirmButtonColor: '#3085d6',
customClass: {
title: 'mb-0'
}
});
}
function confirmDelete(id) {
Swal.fire({
title: 'ยืนยันการลบ?',
text: "คุณไม่สามารถกู้คืนข้อมูลนี้ได้!",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
confirmButtonText: 'ใช่, ลบเลย!',
cancelButtonText: 'ยกเลิก'
}).then((result) => {
if (result.isConfirmed) {
window.location.href = `assignments.php?delete=${id}`;
}
})
}
</script>
<?php elseif ($action === 'add' || $action === 'edit'):
$editMode = ($action === 'edit' && isset($_GET['id']));
$item = null;
if ($editMode) {
$stmt = $pdo->prepare("SELECT * FROM assignments WHERE id = ?");
$stmt->execute([$_GET['id']]);
$item = $stmt->fetch();
if (!$item) {
echo "<script>window.location.href='assignments.php';</script>";
exit;
}
}
?>
<div class="bg-white rounded-lg shadow-md p-6 max-w-2xl mx-auto">
<h3 class="text-xl font-bold mb-4 border-b pb-2"><?= $editMode ? 'แก้ไขงาน' : 'เพิ่มงานใหม่' ?></h3>
<form method="POST" action="assignments.php">
<?php if ($editMode): ?>
<input type="hidden" name="id" value="<?= $item['id'] ?>">
<?php endif; ?>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="title">ชื่องาน/ชื่อวิชา <span class="text-red-500">*</span></label>
<input type="text" name="title" id="title" value="<?= $editMode ? htmlspecialchars($item['title']) : '' ?>" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:ring-2 focus:ring-blue-500" required>
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="description">รายละเอียดงาน</label>
<textarea name="description" id="description" rows="3" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:ring-2 focus:ring-blue-500"><?= $editMode ? htmlspecialchars($item['description']) : '' ?></textarea>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label class="block text-gray-700 text-sm font-bold mb-2" for="due_date">วันที่กำหนดส่ง <span class="text-red-500">*</span></label>
<input type="date" name="due_date" id="due_date" value="<?= $editMode ? $item['due_date'] : '' ?>" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:ring-2 focus:ring-blue-500" required>
</div>
<div class="md:col-span-2">
<label class="block text-gray-700 text-sm font-bold mb-2" for="student_ids">มอบหมายให้นักเรียน <span class="text-red-500">*</span></label>
<div class="border rounded shadow-sm">
<div class="p-2 border-b bg-gray-50 flex justify-between items-center">
<input type="text" id="search_student" placeholder="ค้นหาชื่อ-สกุล..." class="text-sm px-2 py-1 border rounded w-2/3 focus:outline-none focus:border-blue-500">
<label class="flex items-center text-sm font-bold text-blue-600 cursor-pointer">
<input type="checkbox" id="select_all_students" class="mr-2"> เลือกทั้งหมด
</label>
</div>
<div class="max-h-48 overflow-y-auto p-2" id="student_list">
<?php foreach ($students as $student): ?>
<label class="flex items-center p-2 hover:bg-blue-50 rounded cursor-pointer student-item">
<input type="checkbox" name="student_ids[]" value="<?= $student['id'] ?>" class="mr-3 student-checkbox" <?= ($editMode && $item['student_id'] == $student['id']) ? 'checked' : '' ?>>
<span class="student-name text-gray-700"><?= htmlspecialchars($student['name']) ?></span>
</label>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
<?php if ($editMode): ?>
<div class="mb-6">
<label class="block text-gray-700 text-sm font-bold mb-2" for="status">สถานะ</label>
<select name="status" id="status" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="pending" <?= $item['status'] === 'pending' ? 'selected' : '' ?>>รอดำเนินการ</option>
<option value="submitted" <?= $item['status'] === 'submitted' ? 'selected' : '' ?>>ส่งแล้ว</option>
</select>
</div>
<?php endif; ?>
<div class="flex items-center justify-end border-t pt-4 mt-6">
<a href="assignments.php" class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded mr-2">ยกเลิก</a>
<button type="submit" name="<?= $editMode ? 'edit' : 'add' ?>" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
<?= $editMode ? 'บันทึกการแก้ไข' : 'เพิ่มงาน' ?>
</button>
</div>
</form>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
const searchInput = document.getElementById('search_student');
const selectAllCheckbox = document.getElementById('select_all_students');
const studentItems = document.querySelectorAll('.student-item');
const studentCheckboxes = document.querySelectorAll('.student-checkbox');
if (searchInput) {
searchInput.addEventListener('input', function(e) {
const term = e.target.value.toLowerCase();
studentItems.forEach(item => {
const name = item.querySelector('.student-name').textContent.toLowerCase();
if (name.includes(term)) {
item.style.display = 'flex';
} else {
item.style.display = 'none';
}
});
});
}
if (selectAllCheckbox) {
selectAllCheckbox.addEventListener('change', function(e) {
const isChecked = e.target.checked;
studentCheckboxes.forEach(cb => {
const parent = cb.closest('.student-item');
if (parent.style.display !== 'none') {
cb.checked = isChecked;
}
});
});
}
});
</script>
<?php include 'includes/footer.php'; ?>
@@ -0,0 +1,79 @@
<?php
session_start();
$db_host = 'localhost';
$db_user = 'root'; // Change if needed
$db_pass = '@Samui@10742'; // Change if needed
$db_name = 'student_assignment_notify';
try {
$pdo = new PDO("mysql:host=$db_host;dbname=$db_name;charset=utf8mb4", $db_user, $db_pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$pdo->exec("CREATE TABLE IF NOT EXISTS settings (
setting_key VARCHAR(50) PRIMARY KEY,
setting_value TEXT
)");
$pdo->exec("CREATE TABLE IF NOT EXISTS activity_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NULL,
action VARCHAR(100) NOT NULL,
details TEXT,
ip_address VARCHAR(45),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
} catch (PDOException $e) {
die("Database Connection failed: " . $e->getMessage());
}
// Helper function to get setting
function getSetting($pdo, $key)
{
$stmt = $pdo->prepare("SELECT setting_value FROM settings WHERE setting_key = ?");
$stmt->execute([$key]);
$result = $stmt->fetch();
return $result ? $result['setting_value'] : null;
}
// Helper function to log activity
function logActivity($pdo, $action, $details = '')
{
$user_id = $_SESSION['user_id'] ?? null;
$ip_address = $_SERVER['REMOTE_ADDR'] ?? 'Unknown';
try {
$stmt = $pdo->prepare("INSERT INTO activity_logs (user_id, action, details, ip_address) VALUES (?, ?, ?, ?)");
$stmt->execute([$user_id, $action, $details, $ip_address]);
} catch (PDOException $e) {
// Silently ignore log insertion errors to prevent breaking the app
}
}
// Helper function to check login
function isLoggedIn()
{
return isset($_SESSION['user_id']);
}
function requireLogin()
{
if (!isLoggedIn()) {
header('Location: index.php');
exit;
}
}
function isAdmin()
{
return isset($_SESSION['role']) && $_SESSION['role'] === 'admin';
}
function requireAdmin()
{
if (!isAdmin()) {
die("Unauthorized access.");
}
}
@@ -0,0 +1,122 @@
<?php
// This script is meant to be run via Cron Job daily (e.g., at 08:00 AM)
// It checks for pending assignments close to the deadline and sends notifications.
require_once 'config.php';
// Prevent rendering layout if accessed via web (can be protected with a key in production)
echo "<h1>ระบบแจ้งเตือนอัตโนมัติ (Cron Job)</h1>";
echo "<pre>";
// Get settings
$days_before_due = getSetting($pdo, 'days_before_due') ?: 3;
$telegram_bot_token = getSetting($pdo, 'telegram_bot_token');
$telegram_group_chat_id = getSetting($pdo, 'telegram_group_chat_id');
$line_notify_token = getSetting($pdo, 'line_notify_token');
$notify_via_line = getSetting($pdo, 'notify_via_line') !== '0';
$notify_via_telegram = getSetting($pdo, 'notify_via_telegram') !== '0';
// Function to send LINE Notify
function sendLineNotify($token, $message) {
if (empty($token)) return false;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://notify-api.line.me/api/notify");
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, "message=" . urlencode($message));
$headers = array('Content-type: application/x-www-form-urlencoded', 'Authorization: Bearer ' . $token);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result = curl_exec($ch);
curl_close($ch);
return json_decode($result, true);
}
// Function to send Telegram Message
function sendTelegramMessage($botToken, $chatId, $message) {
if (empty($botToken) || empty($chatId)) return false;
$url = "https://api.telegram.org/bot{$botToken}/sendMessage";
$data = [
'chat_id' => $chatId,
'text' => $message,
'parse_mode' => 'HTML'
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($ch);
curl_close($ch);
return json_decode($result, true);
}
// Fetch pending assignments that need notification
// We don't need line_token or telegram_chat_id from users anymore for external notification.
$sql = "SELECT a.id, a.title, a.due_date, u.name, DATEDIFF(a.due_date, CURDATE()) as days_left
FROM assignments a
JOIN users u ON a.student_id = u.id
WHERE a.status = 'pending'
AND DATEDIFF(a.due_date, CURDATE()) = :days_before_due";
$stmt = $pdo->prepare($sql);
$stmt->execute([':days_before_due' => $days_before_due]);
$assignments = $stmt->fetchAll();
echo "พบงานที่ต้องแจ้งเตือน: " . count($assignments) . " รายการ (เหลือเวลา $days_before_due วัน)\n\n";
$success_line = 0;
$success_tg = 0;
foreach ($assignments as $task) {
// Send via LINE (GROUP ONLY)
if ($notify_via_line && !empty($line_notify_token)) {
$message = "⚠️ แจ้งเตือนกำหนดส่งงาน ⚠️\n";
$message .= "เรียนคุณ: {$task['name']}\n";
$message .= "งาน: {$task['title']}\n";
$message .= "กำหนดส่ง: " . date('d/m/Y', strtotime($task['due_date'])) . "\n";
$message .= "สถานะ: เหลือเวลาอีก {$task['days_left']} วัน!\n";
$message .= "กรุณาตรวจสอบระบบ";
$res = sendLineNotify($line_notify_token, $message);
if (isset($res['status']) && $res['status'] == 200) {
$success_line++;
echo "✅ LINE Group: แจ้งเตือน '{$task['title']}' สำหรับ '{$task['name']}' สำเร็จ\n";
} else {
echo "❌ LINE Group: แจ้งเตือน '{$task['title']}' สำหรับ '{$task['name']}' ล้มเหลว\n";
}
}
// Send via Telegram (GROUP ONLY)
if ($notify_via_telegram && !empty($telegram_bot_token) && !empty($telegram_group_chat_id)) {
$tgMessage = "⚠️ <b>แจ้งเตือนกำหนดส่งงาน</b> ⚠️\n\n";
$tgMessage .= "👤 เรียนคุณ: {$task['name']}\n";
$tgMessage .= "📝 งาน: <b>{$task['title']}</b>\n";
$tgMessage .= "📅 กำหนดส่ง: " . date('d/m/Y', strtotime($task['due_date'])) . "\n";
$tgMessage .= "⏳ สถานะ: <b>เหลือเวลาอีก {$task['days_left']} วัน!</b>\n";
$res = sendTelegramMessage($telegram_bot_token, $telegram_group_chat_id, $tgMessage);
if (isset($res['ok']) && $res['ok'] == 1) {
$success_tg++;
echo "✅ Telegram Group: แจ้งเตือน '{$task['title']}' สำหรับ '{$task['name']}' สำเร็จ\n";
} else {
echo "❌ Telegram Group: แจ้งเตือน '{$task['title']}' สำหรับ '{$task['name']}' ล้มเหลว\n";
}
}
}
// Fetch OVERDUE assignments (optional, just to show how it could be done)
$sql_overdue = "SELECT a.id, a.title, u.name, u.line_token, DATEDIFF(CURDATE(), a.due_date) as days_overdue
FROM assignments a JOIN users u ON a.student_id = u.id
WHERE a.status = 'pending' AND a.due_date < CURDATE()";
// ... execution skipped to keep output clean, but can be added
echo "\n--- สรุปผลการแจ้งเตือน ---\n";
echo "LINE: ส่งสำเร็จ $success_line รายการ\n";
echo "Telegram: ส่งสำเร็จ $success_tg รายการ\n";
echo "</pre>";
?>
@@ -0,0 +1,198 @@
<?php
require_once 'config.php';
requireLogin();
$user_id = $_SESSION['user_id'];
$role = $_SESSION['role'];
// Get statistics
$stats = [];
if ($role === 'admin') {
$stmt = $pdo->query("SELECT COUNT(*) FROM assignments");
$stats['total_assignments'] = $stmt->fetchColumn();
$stmt = $pdo->query("SELECT COUNT(*) FROM assignments WHERE status = 'pending'");
$stats['pending_assignments'] = $stmt->fetchColumn();
$stmt = $pdo->query("SELECT COUNT(*) FROM assignments WHERE status = 'completed'");
$stats['completed_assignments'] = $stmt->fetchColumn();
$stmt = $pdo->query("SELECT COUNT(*) FROM users WHERE role = 'student'");
$stats['total_students'] = $stmt->fetchColumn();
} else {
$stmt = $pdo->prepare("SELECT COUNT(*) FROM assignments WHERE student_id = ?");
$stmt->execute([$user_id]);
$stats['total_assignments'] = $stmt->fetchColumn();
$stmt = $pdo->prepare("SELECT COUNT(*) FROM assignments WHERE student_id = ? AND status = 'pending'");
$stmt->execute([$user_id]);
$stats['pending_assignments'] = $stmt->fetchColumn();
$stmt = $pdo->prepare("SELECT COUNT(*) FROM assignments WHERE student_id = ? AND status = 'completed'");
$stmt->execute([$user_id]);
$stats['completed_assignments'] = $stmt->fetchColumn();
}
include 'includes/header.php';
include 'includes/sidebar.php';
?>
<div class="mb-6 flex justify-between items-center">
<div>
<h2 class="text-2xl font-bold text-gray-800">ยินดีต้อนรับ, <?= htmlspecialchars($_SESSION['name']) ?>!</h2>
<p class="text-gray-600 mt-1">ภาพรวมระบบการแจ้งเตือนงานของคุณ</p>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
<div class="bg-white rounded-lg shadow-md p-5 border-l-4 border-blue-500 cursor-pointer hover:bg-blue-50 transition" onclick="filterTable('all', 'งานทั้งหมด')">
<div class="flex items-center">
<div class="p-3 rounded-full bg-blue-100 text-blue-500 mr-4">
<i class="fas fa-tasks fa-xl"></i>
</div>
<div>
<p class="text-gray-500 text-xs font-semibold uppercase">งานทั้งหมด</p>
<p class="text-xl font-bold text-gray-800"><?= $stats['total_assignments'] ?></p>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow-md p-5 border-l-4 border-yellow-500 cursor-pointer hover:bg-yellow-50 transition" onclick="filterTable('pending', 'งานที่ค้างส่ง (รอดำเนินการ)')">
<div class="flex items-center">
<div class="p-3 rounded-full bg-yellow-100 text-yellow-500 mr-4">
<i class="fas fa-clock fa-xl"></i>
</div>
<div>
<p class="text-gray-500 text-xs font-semibold uppercase">รอดำเนินการ</p>
<p class="text-xl font-bold text-gray-800"><?= $stats['pending_assignments'] ?></p>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow-md p-5 border-l-4 border-green-500 cursor-pointer hover:bg-green-50 transition" onclick="filterTable('completed', 'งานที่เสร็จสิ้นแล้ว')">
<div class="flex items-center">
<div class="p-3 rounded-full bg-green-100 text-green-500 mr-4">
<i class="fas fa-check-circle fa-xl"></i>
</div>
<div>
<p class="text-gray-500 text-xs font-semibold uppercase">เสร็จสิ้น</p>
<p class="text-xl font-bold text-gray-800"><?= $stats['completed_assignments'] ?></p>
</div>
</div>
</div>
<?php if ($role === 'admin'): ?>
<div class="bg-white rounded-lg shadow-md p-5 border-l-4 border-purple-500 cursor-pointer hover:bg-purple-50 transition" onclick="window.location.href='users.php'">
<div class="flex items-center">
<div class="p-3 rounded-full bg-purple-100 text-purple-500 mr-4">
<i class="fas fa-users fa-xl"></i>
</div>
<div>
<p class="text-gray-500 text-xs font-semibold uppercase">นักเรียนทั้งหมด</p>
<p class="text-xl font-bold text-gray-800"><?= $stats['total_students'] ?></p>
</div>
</div>
</div>
<?php else: ?>
<div class="bg-white rounded-lg shadow-md p-5 border-l-4 border-red-500 cursor-pointer hover:bg-red-50 transition" onclick="filterTable('urgent', 'งานที่ใกล้กำหนดส่ง')">
<div class="flex items-center">
<div class="p-3 rounded-full bg-red-100 text-red-500 mr-4">
<i class="fas fa-exclamation-triangle fa-xl"></i>
</div>
<div>
<p class="text-gray-500 text-xs font-semibold uppercase">ใกล้กำหนดส่ง</p>
<p class="text-xl font-bold text-gray-800">-</p>
</div>
</div>
</div>
<?php endif; ?>
</div>
<div class="bg-white rounded-lg shadow-md p-6">
<div class="flex justify-between items-center mb-4 pb-2 border-b">
<h3 id="tableTitle" class="text-lg font-bold text-gray-800"><i class="fas fa-exclamation-circle text-red-500 mr-2"></i> งานที่ใกล้กำหนดส่ง</h3>
<button onclick="filterTable('urgent', 'งานที่ใกล้กำหนดส่ง')" class="text-xs bg-gray-100 hover:bg-gray-200 text-gray-700 py-1 px-2 rounded border">
<i class="fas fa-filter"></i> คืนค่า (ใกล้กำหนดส่ง)
</button>
</div>
<table id="dashboardTable" class="dataTable display min-w-full bg-white w-full">
<thead class="bg-gray-100 text-gray-600">
<tr>
<th class="py-3 px-4 text-center font-semibold text-sm border-b w-16">ลำดับ</th>
<th class="py-3 px-4 text-center font-semibold text-sm border-b">ชื่องาน/ชื่อวิชา</th>
<?php if ($role === 'admin'): ?>
<th class="py-3 px-4 text-center font-semibold text-sm border-b">นักเรียน</th>
<?php endif; ?>
<th class="py-3 px-4 text-center font-semibold text-sm border-b w-32">วันที่เพิ่มข้อมูล</th>
<th class="py-3 px-4 text-center font-semibold text-sm border-b">กำหนดส่ง</th>
<th class="py-3 px-4 text-center font-semibold text-sm border-b w-24">สถานะ</th>
</tr>
</thead>
<tbody class="text-gray-700">
<!-- Data will be loaded via AJAX -->
</tbody>
</table>
</div>
<!-- DataTables Export Plugins -->
<script src="https://cdn.datatables.net/buttons/2.4.1/js/dataTables.buttons.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<script src="https://cdn.datatables.net/buttons/2.4.1/js/buttons.html5.min.js"></script>
<script>
let dashboardTable;
$(document).ready(function() {
// Determine columns based on role
const columns = [
{ data: 'index', className: 'text-center' },
{ data: 'title' },
<?php if ($role === 'admin'): ?>
{ data: 'student' },
<?php endif; ?>
{ data: 'created_at', className: 'text-center' },
{ data: 'due_date', className: 'text-center' },
{ data: 'status', className: 'text-center' }
];
dashboardTable = $('#dashboardTable').DataTable({
ajax: 'api/get_dashboard_table.php?filter=urgent',
columns: columns,
scrollX: true,
columnDefs: [{ className: "dt-head-center", targets: "_all" }],
pageLength: 10,
dom: '<"flex justify-between items-center mb-4"lBf>rt<"flex justify-between items-center mt-4"ip>',
buttons: [
{
extend: 'excelHtml5',
text: '<i class="fas fa-file-excel mr-1"></i> ส่งออก Excel',
className: 'bg-green-600 text-white hover:bg-green-700 px-3 py-1 rounded text-sm',
title: 'รายงานการส่งงาน',
exportOptions: {
columns: ':visible'
}
}
],
language: {
url: '//cdn.datatables.net/plug-ins/1.13.6/i18n/th.json'
}
});
});
function filterTable(filterType, titleText) {
// Update Title
let icon = '';
if (filterType === 'urgent') icon = '<i class="fas fa-exclamation-circle text-red-500 mr-2"></i>';
else if (filterType === 'all') icon = '<i class="fas fa-tasks text-blue-500 mr-2"></i>';
else if (filterType === 'pending') icon = '<i class="fas fa-clock text-yellow-500 mr-2"></i>';
else if (filterType === 'completed') icon = '<i class="fas fa-check-circle text-green-500 mr-2"></i>';
$('#tableTitle').html(icon + ' ' + titleText);
// Reload DataTable
dashboardTable.ajax.url('api/get_dashboard_table.php?filter=' + filterType).load();
}
</script>
<?php include 'includes/footer.php'; ?>
@@ -0,0 +1,71 @@
-- Create database if you haven't already
-- CREATE DATABASE IF NOT EXISTS `assignment_notifier` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- USE `assignment_notifier`;
-- --------------------------------------------------------
-- Table structure for table `users`
-- --------------------------------------------------------
CREATE TABLE IF NOT EXISTS `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password` varchar(255) NOT NULL,
`role` enum('admin','student') NOT NULL DEFAULT 'student',
`name` varchar(100) NOT NULL,
`line_token` varchar(100) DEFAULT NULL,
`telegram_chat_id` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Insert default admin and student
INSERT INTO `users` (`username`, `password`, `role`, `name`) VALUES
('admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'admin', 'Administrator'), -- Password is 'password'
('student1', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'student', 'Student One') -- Password is 'password'
ON DUPLICATE KEY UPDATE id=id;
-- --------------------------------------------------------
-- Table structure for table `assignments`
-- --------------------------------------------------------
CREATE TABLE IF NOT EXISTS `assignments` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) NOT NULL,
`description` text,
`due_date` date NOT NULL,
`student_id` int(11) NOT NULL,
`status` enum('pending','completed') NOT NULL DEFAULT 'pending',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `student_id` (`student_id`),
CONSTRAINT `assignments_ibfk_1` FOREIGN KEY (`student_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- --------------------------------------------------------
-- Table structure for table `settings`
-- --------------------------------------------------------
CREATE TABLE IF NOT EXISTS `settings` (
`setting_key` varchar(50) NOT NULL,
`setting_value` text DEFAULT NULL,
PRIMARY KEY (`setting_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Insert default settings
INSERT INTO `settings` (`setting_key`, `setting_value`) VALUES
('days_before_due', '3'),
('notification_time', '08:00'),
('footer_text', 'ระบบแจ้งเตือนส่งงาน'),
('notify_via_line', '1'),
('notify_via_telegram', '1')
ON DUPLICATE KEY UPDATE setting_key=setting_key;
-- --------------------------------------------------------
-- Table structure for table `activity_logs`
-- --------------------------------------------------------
CREATE TABLE IF NOT EXISTS `activity_logs` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) DEFAULT NULL,
`action` varchar(100) NOT NULL,
`details` text DEFAULT NULL,
`ip_address` varchar(45) DEFAULT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@@ -0,0 +1,24 @@
เอกสารอธิบายการทำงานของระบบแจ้งเตือนส่งงาน
=========================================
1. การทำงานของระบบแจ้งเตือน และวิธีการแจ้งเตือนผู้ใช้เมื่อใกล้กำหนดส่ง
ระบบแจ้งเตือนถูกออกแบบมาเพื่อป้องกันการลืมส่งงานของนักเรียน โดยแบ่งการทำงานออกเป็น 2 ช่องทางหลัก:
- แจ้งเตือนบนหน้าเว็บไซต์ (In-app Popup Alert): ทันทีที่นักเรียนเข้าสู่ระบบ (Login) สำเร็จ ระบบจะเช็คข้อมูลงานทั้งหมดที่สถานะยังเป็น "รอดำเนินการ" และตรวจสอบว่ามีงานใดที่เข้าข่าย "ใกล้ถึงกำหนดส่ง" หรือ "เลยกำหนดส่ง" ไปแล้ว หากพบระบบจะเด้งหน้าต่าง Popup แสดงรายการงานเหล่านั้นขึ้นมากลางหน้าจอทันที (โดยจะแสดงแค่ 1 ครั้งต่อการล็อกอิน เพื่อไม่ให้รบกวนการใช้งานหน้าอื่นๆ)
- แจ้งเตือนผ่านแอปพลิเคชันแชท (LINE Notify / Telegram): ระบบจะมีสคริปต์ทำงานอยู่เบื้องหลัง (Cron Job) ที่จะถูกสั่งให้รันตามเวลาที่กำหนดไว้ทุกวัน (เช่น 08:00 น. ของทุกวัน) สคริปต์นี้จะกวาดข้อมูลงานที่ใกล้ถึงกำหนดส่งทั้งหมด แล้วส่งข้อความสรุปแจ้งเตือนเข้าไปยังกลุ่มแชทของห้องเรียนผ่าน API แบบอัตโนมัติ
2. วิธีการนำวันและเวลามาใช้ เพื่อคำนวณและแจ้งเตือน
ระบบใช้หลักการเปรียบเทียบ "วันปัจจุบัน" กับ "กำหนดส่ง" เพื่อสร้างเงื่อนไขการแจ้งเตือนดังนี้:
- การเก็บข้อมูล: วันกำหนดส่ง (Due Date) จะถูกบันทึกลงในฐานข้อมูลในรูปแบบ YYYY-MM-DD
- การหาจำนวนวันที่เหลือ: ระบบใช้ฟังก์ชันหาส่วนต่างของวัน (เช่น DATEDIFF ใน SQL) โดยนำ "วันที่กำหนดส่ง" มาลบด้วย "วันที่ปัจจุบัน" เพื่อหาค่า "จำนวนวันที่เหลือ"
- เงื่อนไขการแจ้งเตือน: แอดมินสามารถกำหนดค่า "จำนวนวันล่วงหน้าที่จะให้ระบบเริ่มเตือน" ได้ในหน้าตั้งค่า (ตัวอย่างเช่น กำหนดไว้ 3 วัน)
* ถ้า (วันกำหนดส่ง - วันปัจจุบัน) < 0 หมายถึง "เลยกำหนดส่ง" ระบบจะขึ้นป้ายสีดำ
* ถ้า (วันกำหนดส่ง - วันปัจจุบัน) <= 3 หมายถึง "ใกล้กำหนดส่ง" ระบบจะขึ้นป้ายสีส้ม และนำงานนี้ไปใส่ใน Popup แจ้งเตือน และส่งเข้าแชท LINE/Telegram
* ถ้าเหลือมากกว่า 3 วัน หมายถึงเวลายังเหลือเยอะ ระบบจะยังไม่แจ้งเตือน เพื่อป้องกันข้อความขยะ (Spam)
3. ส่วนประกอบหลักของระบบ
ระบบถูกแบ่งการทำงานออกเป็นส่วนต่างๆ อย่างชัดเจน ดังนี้:
- หน้าบันทึกงาน (หน้า "จัดการงาน"): เป็นส่วนของแอดมินหรือครู มีฟอร์มสำหรับเพิ่มงานใหม่ ระบุชื่องาน/ชื่อวิชา, ใส่รายละเอียด, เลือกตัวนักเรียน, และตั้งวันกำหนดส่ง ทันทีที่บันทึก ข้อมูลจะถูกลงฐานข้อมูลและเริ่มเข้าสู่กระบวนการนับถอยหลังทันที
- หน้าดูรายการงาน (หน้า "Dashboard" และ "งานของฉัน"): เป็นหน้าแสดงผลรายการงานทั้งหมดในรูปแบบตาราง (DataTables) โดยมีการเรียงลำดับจากงานที่ถูกสั่งล่าสุด (รัน Running Number) และแสดง "ป้ายสี" ท้ายวันที่กำหนดส่งเพื่อบอกสถานะความเร่งด่วน
- หน้า/ส่วนของการแจ้งเตือน (Notification Module):
* แจ้งเตือนฝั่งผู้ใช้งานเว็บ: มีการฝังสคริปต์ไว้ในส่วน Footer ของเว็บ เมื่อนักเรียนล็อกอิน ระบบจะเรียกใช้ AJAX ไปดึงข้อมูลงานที่เข้าเงื่อนไขเตือน มาแสดงเป็นกราฟิกสวยงามผ่าน SweetAlert2
* แจ้งเตือนฝั่งแชท: มีไฟล์ cron_notify.php ที่ทำหน้าที่สรุปข้อมูลเป็นข้อความตัวอักษร และยิงข้อมูลไปที่ Server ของ LINE หรือ Telegram ตาม Token ที่แอดมินตั้งค่าไว้
@@ -0,0 +1,53 @@
เอกสารอธิบายการทำงานของระบบแจ้งเตือนส่งงาน (ฉบับเข้าใจง่ายสำหรับผู้เริ่มต้น/นักเรียน)
========================================================================
สวัสดีครับ! ถ้าน้องๆ กำลังสงสัยว่า "เว็บไซต์ที่ช่วยเตือนเราส่งงานนี้ มันทำงานได้ยังไงเบื้องหลัง?" เอกสารฉบับนี้จะช่วยอธิบายให้เห็นภาพรวมของระบบคอมพิวเตอร์และการเขียนโปรแกรมแบบง่ายๆ โดยไม่ต้องมีพื้นฐานมาก่อนเลยครับ
---------------------------------------------------------
1. ระบบแจ้งเตือนทำงานอย่างไร และมันรู้ได้ยังไงว่าต้องเตือนเรา?
---------------------------------------------------------
เวลาเราใช้งานเว็บนี้ ระบบจะเตือนเราผ่าน 2 วิธี ซึ่งทำงานแยกกัน:
แบบที่ 1: แจ้งเตือนเด้งขึ้นมาบนหน้าเว็บ (Popup Alert)
- เปรียบเทียบง่ายๆ: เหมือนพนักงานต้อนรับที่จะทักทายเราเฉพาะตอนที่เราเดินเข้าประตูมา
- ทำงานอย่างไร?: ทันทีที่น้องๆ พิมพ์ชื่อผู้ใช้และรหัสผ่านเพื่อ "เข้าสู่ระบบ (Login)" โปรแกรมจะแอบวิ่งไปเปิด "สมุดจด (ฐานข้อมูล หรือ Database)" เพื่อดูว่าเรามีงานอะไรที่ค้างอยู่และใกล้ถึงวันส่งหรือเปล่า ถ้ามี โค้ดส่วนหน้าเว็บ (เราเรียกว่า Frontend) จะวาดกล่องข้อความเด้งขึ้นมากลางจอทันที แต่ถ้าไม่มีก็ผ่านไป
แบบที่ 2: แจ้งเตือนผ่านแอปแชท (LINE / Telegram)
- เปรียบเทียบง่ายๆ: เหมือนมีหุ่นยนต์ผู้ช่วย (Bot) ประจำห้องเรียน
- ทำงานอย่างไร?: หุ่นยนต์ตัวนี้ไม่ได้เฝ้าหน้าเว็บตลอดเวลา แต่เราตั้งนาฬิกาปลุกให้มันตื่นขึ้นมาทำงาน "ทุกเช้าเวลา 08:00 น." (ในภาษาคอมพิวเตอร์ เราเรียกการตั้งเวลาให้โปรแกรมทำงานเองว่า Cron Job) เมื่อมันตื่น มันจะเช็คงานของทุกคนในห้อง ถ้างบานไหนใกล้กำหนดส่ง มันจะรีบส่งข้อความเข้าไปในกลุ่ม LINE ทันทีแล้วก็กลับไปนอนต่อ
---------------------------------------------------------
2. เว็บไซต์เอา "วันและเวลา" มาคำนวณได้อย่างไร? (คณิตศาสตร์ของโปรแกรม)
---------------------------------------------------------
คอมพิวเตอร์ไม่ได้มีความรู้สึกว่า "ใกล้ถึงวันส่งหรือยัง" มันรู้แค่ตัวเลข! ดังนั้นโปรแกรมเมอร์จึงใช้วิธีสอนคอมพิวเตอร์ด้วยคณิตศาสตร์พื้นฐานครับ:
ขั้นตอนที่ A (จดจำ):
เมื่อคุณครูสั่งงาน ระบบจะบันทึก "วันกำหนดส่ง (Due Date)" เอาไว้ในรูปแบบตัวเลข เช่น 2026-10-15 (ปี-เดือน-วัน)
ขั้นตอนที่ B (คำนวณหาส่วนต่าง):
ทุกครั้งที่ระบบเช็คข้อมูล มันจะถามคอมพิวเตอร์ว่า "วันนี้วันที่เท่าไหร่?" (Today) จากนั้นมันจะจับตัวเลขมาลบกัน:
[วันกำหนดส่ง] ลบ [วันปัจจุบัน] = จำนวนวันที่เหลือ (Days Left)
ขั้นตอนที่ C (การสร้างเงื่อนไข หรือ If-Else):
สมมติว่าคุณครูตั้งค่าไว้ว่า "ให้เตือนล่วงหน้า 3 วันนะ" โปรแกรมจะใช้ตรรกะแบบนี้ครับ:
- ถ้า (If) จำนวนวันที่เหลือติดลบ (< 0) -> แปลว่า "เลยกำหนดแล้ว!" ระบบจะสั่งระบายสีดำ
- ถ้า (If) จำนวนวันที่เหลือน้อยกว่าหรือเท่ากับ 3 (<= 3) -> แปลว่า "ใกล้กำหนดแล้ว!" ระบบจะสั่งระบายสีส้ม และเอาไปแจ้งเตือน
- ไม่อย่างนั้น (Else) ถ้ายังเหลือเวลาอีกนาน -> แปลว่า "ยังชิลได้" ระบบจะไม่ส่งแจ้งเตือนอะไรไปกวนใจนักเรียน
---------------------------------------------------------
3. ชิ้นส่วนต่างๆ ของเว็บไซต์ (ส่วนประกอบของระบบ)
---------------------------------------------------------
เวลาเราสร้างเว็บๆ หนึ่ง มันเกิดจากการเอาชิ้นส่วนหลายๆ ชิ้นมาประกอบกัน เหมือนต่อเลโก้ครับ:
ส่วนที่ 1: หน้าบันทึกงาน (ส่วนนำเข้าข้อมูล / Input)
คือหน้าเว็บที่มี "ฟอร์ม (Form)" ให้คุณครูพิมพ์ชื่องาน เลือกวันที่ และกดปุ่มตกลง หน้าที่ของหน้านี้คือรับข้อมูลจากมนุษย์ และแปลงไปเก็บไว้ใน "ฐานข้อมูล (Database)" ซึ่งเปรียบเสมือนตู้เก็บเอกสารดิจิทัล
ส่วนที่ 2: หน้าดูรายการงาน (ส่วนแสดงผลข้อมูล / Output)
คือหน้า Dashboard หรือหน้างานของฉัน ที่แสดงข้อมูลออกมาเป็น "ตาราง" โค้ดในส่วนนี้ (เราใช้ภาษา PHP) จะไปค้นหาเอกสารในตู้เก็บเอกสาร ดึงข้อมูลออกมาจัดเรียงให้สวยงาม ใส่สี (CSS/Tailwind) รันลำดับที่ 1,2,3 ให้ดูง่าย และวาดลงบนหน้าจอคอมพิวเตอร์หรือมือถือของเรา
ส่วนที่ 3: ระบบแจ้งเตือน (ส่วนประสานงาน / API & Background)
เป็นส่วนที่มองไม่เห็นบนหน้าเว็บ ประกอบด้วย "API" ซึ่งเป็นสะพานเชื่อมต่อให้เว็บไซต์ของเราสามารถโยนข้อความข้ามไปโผล่ในเครื่องเซิร์ฟเวอร์ของ LINE หรือ Telegram ได้นั่นเองครับ!
---------------------------------------------------------
สรุปสั้นๆ สำหรับนักเรียนที่อยากเป็นโปรแกรมเมอร์:
"ระบบนี้คือการนำ ข้อมูล (งาน) มาเก็บไว้ในตู้ (Database) จากนั้นใช้ คณิตศาสตร์ (วันลบกัน) มาสร้างเป็น เงื่อนไข (If-Else) เพื่อสั่งให้หน้าเว็บ (HTML/CSS) หรือ หุ่นยนต์ (LINE Bot) แสดงผลออกมาให้มนุษย์เห็นนั่นเองครับ!"
@@ -0,0 +1,84 @@
</main>
<?php
// Fetch footer text
$footer_text_display = 'ระบบแจ้งเตือนส่งงาน';
if (isset($pdo)) {
try {
$stmt_footer = $pdo->prepare("SELECT setting_value FROM settings WHERE setting_key = 'footer_text'");
$stmt_footer->execute();
if ($row_footer = $stmt_footer->fetch()) {
$footer_text_display = $row_footer['setting_value'];
}
} catch (Exception $e) {}
}
?>
<footer class="bg-white border-t border-gray-200 p-4 text-center text-sm text-gray-600 shrink-0">
&copy; <?= date('Y') ?> <?= htmlspecialchars($footer_text_display) ?>
</footer>
</div>
</div>
<!-- DataTables JS -->
<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>
// Initialize DataTables globally if table exists and not already initialized
$(document).ready(function() {
if ($('.dataTable').length > 0) {
$('.dataTable').each(function() {
if (!$.fn.dataTable.isDataTable(this)) {
$(this).DataTable({
responsive: true,
columnDefs: [{ className: "dt-head-center", targets: "_all" }],
language: {
url: '//cdn.datatables.net/plug-ins/1.13.6/i18n/th.json',
}
});
}
});
}
});
</script>
<?php if (isset($_SESSION['login_notified']) && $_SESSION['login_notified'] === false): ?>
<script>
// Show notification only once after login
$(document).ready(function() {
$.ajax({
url: 'api/get_notifications.php',
method: 'GET',
dataType: 'json',
success: function(response) {
if(response.status === 'success' && response.data.length > 0) {
let htmlList = '<div style="max-height: 200px; overflow-y: auto;" class="custom-scrollbar"><ul class="text-left mt-2 pr-2">';
response.data.forEach(function(item) {
htmlList += `<li class="mb-2 pb-2 border-b border-gray-200">
<strong>${item.title}</strong><br>
<span class="text-sm text-red-500">กำหนดส่ง: ${item.due_date} (เหลือ ${item.days_left} วัน)</span>
</li>`;
});
htmlList += '</ul></div>';
Swal.fire({
title: 'คุณมีงานใกล้ถึงกำหนดส่ง!',
html: htmlList,
icon: 'warning',
confirmButtonText: 'รับทราบ',
confirmButtonColor: '#3085d6'
});
}
}
});
});
</script>
<?php
// Mark as notified so it doesn't show again in this session
$_SESSION['login_notified'] = true;
endif;
?>
<script src="assets/js/app.js"></script>
</body>
</html>
@@ -0,0 +1,62 @@
<!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" type="image/png" href="assets/img/logo.png">
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/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">
<!-- Custom CSS to make DataTables look good with Tailwind -->
<style>
.dataTables_wrapper .dataTables_paginate .paginate_button {
padding: 0.25em 0.75em;
margin-left: 2px;
border-radius: 0.25rem;
}
.dataTables_wrapper .dataTables_paginate .paginate_button.current {
th, .dataTable th, table.dataTable thead th, table.dataTable thead td { text-align: center !important; }
background: #2563eb !important;
color: white !important;
border: 1px solid #2563eb !important;
}
body { font-family: 'Sarabun', sans-serif; background-color: #f3f4f6; }
.shiny-logo {
position: relative;
overflow: hidden;
display: inline-block;
border-radius: 50%;
}
.shiny-logo::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 6s infinite ease-in-out;
}
@keyframes shine {
0% { left: -150%; }
15% { left: 200%; }
100% { left: 200%; }
}
</style>
<link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<script src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
</head>
<body class="text-gray-800">
<div class="flex h-screen overflow-hidden">
@@ -0,0 +1,85 @@
<!-- Sidebar -->
<div class="bg-gray-800 text-white w-64 flex-shrink-0 flex flex-col hidden md:flex">
<div class="h-16 flex items-center justify-center border-b border-gray-700">
<h1 class="text-xl font-bold flex items-center">
<div class="shiny-logo mr-2">
<img src="assets/img/logo.png" alt="Logo" class="w-8 h-8 object-contain">
</div>
ระบบแจ้งเตือน
</h1>
</div>
<div class="flex-1 overflow-y-auto py-4">
<nav class="px-2 space-y-1">
<?php $currentPage = basename($_SERVER['PHP_SELF']); ?>
<a href="dashboard.php" class="<?= $currentPage == 'dashboard.php' ? 'bg-gray-900 text-white' : 'text-gray-300 hover:bg-gray-700 hover:text-white' ?> group flex items-center px-2 py-2 text-sm font-medium rounded-md">
<i class="fas fa-tachometer-alt mr-3 text-lg text-gray-400 group-hover:text-gray-300"></i>
หน้าแรก (Dashboard)
</a>
<?php if (isAdmin()): ?>
<a href="assignments.php" class="<?= $currentPage == 'assignments.php' ? 'bg-gray-900 text-white' : 'text-gray-300 hover:bg-gray-700 hover:text-white' ?> group flex items-center px-2 py-2 text-sm font-medium rounded-md mt-1">
<i class="fas fa-tasks mr-3 text-lg text-gray-400 group-hover:text-gray-300"></i>
จัดการงาน
</a>
<a href="users.php" class="<?= $currentPage == 'users.php' ? 'bg-gray-900 text-white' : 'text-gray-300 hover:bg-gray-700 hover:text-white' ?> group flex items-center px-2 py-2 text-sm font-medium rounded-md mt-1">
<i class="fas fa-users mr-3 text-lg text-gray-400 group-hover:text-gray-300"></i>
จัดการผู้ใช้
</a>
<a href="settings.php" class="<?= $currentPage == 'settings.php' ? 'bg-gray-900 text-white' : 'text-gray-300 hover:bg-gray-700 hover:text-white' ?> group flex items-center px-2 py-2 text-sm font-medium rounded-md mt-1">
<i class="fas fa-cog mr-3 text-lg text-gray-400 group-hover:text-gray-300"></i>
ตั้งค่าระบบ
</a>
<?php else: ?>
<a href="my_assignments.php" class="<?= $currentPage == 'my_assignments.php' ? 'bg-gray-900 text-white' : 'text-gray-300 hover:bg-gray-700 hover:text-white' ?> group flex items-center px-2 py-2 text-sm font-medium rounded-md mt-1">
<i class="fas fa-tasks mr-3 text-lg text-gray-400 group-hover:text-gray-300"></i>
งานของฉัน
</a>
<?php endif; ?>
<div class="pt-4 mt-4 border-t border-gray-700"></div>
<a href="manual.php" class="<?= $currentPage == 'manual.php' ? 'bg-gray-900 text-white' : 'text-gray-300 hover:bg-gray-700 hover:text-white' ?> group flex items-center px-2 py-2 text-sm font-medium rounded-md">
<i class="fas fa-book mr-3 text-lg text-gray-400 group-hover:text-gray-300"></i>
คู่มือการใช้งาน
</a>
</nav>
</div>
<div class="p-4 border-t border-gray-700">
<div class="flex items-center">
<div>
<img class="inline-block h-9 w-9 rounded-full" src="https://ui-avatars.com/api/?name=<?= urlencode($_SESSION['name']) ?>&background=0D8ABC&color=fff" alt="">
</div>
<div class="ml-3">
<p class="text-sm font-medium text-white"><?= htmlspecialchars($_SESSION['name']) ?></p>
<p class="text-xs font-medium text-gray-400 group-hover:text-gray-300"><?= ucfirst($_SESSION['role']) ?></p>
</div>
</div>
<a href="logout.php" class="mt-4 block text-center bg-red-600 hover:bg-red-700 text-white py-2 px-4 rounded text-sm transition duration-150 ease-in-out">
<i class="fas fa-sign-out-alt mr-1"></i> ออกจากระบบ
</a>
</div>
</div>
<!-- Main Content wrapper -->
<div class="flex-1 flex flex-col overflow-hidden">
<!-- Mobile header -->
<div class="md:hidden bg-gray-800 text-white h-16 flex items-center justify-between px-4">
<h1 class="text-xl font-bold flex items-center">
<div class="shiny-logo mr-2">
<img src="assets/img/logo.png" alt="Logo" class="w-8 h-8 object-contain">
</div>
ระบบแจ้งเตือน
</h1>
<button id="mobile-menu-button" class="text-gray-300 hover:text-white focus:outline-none focus:text-white">
<i class="fas fa-bars text-2xl"></i>
</button>
</div>
<!-- Main content -->
<main class="flex-1 overflow-x-hidden overflow-y-auto bg-gray-100 p-6">
@@ -0,0 +1,123 @@
<?php
require_once 'config.php';
if (isLoggedIn()) {
header('Location: dashboard.php');
exit;
}
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
if (empty($username) || empty($password)) {
$error = 'กรุณากรอกชื่อผู้ใช้และรหัสผ่าน';
} else {
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?");
$stmt->execute([$username]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password'])) {
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role'] = $user['role'];
$_SESSION['name'] = $user['name'];
$_SESSION['login_notified'] = false; // Flag for showing popup once
logActivity($pdo, 'LOGIN_SUCCESS', "User logged in successfully");
header('Location: dashboard.php');
exit;
} else {
$error = 'ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง';
// Log failed attempt without a session
$ip = $_SERVER['REMOTE_ADDR'] ?? 'Unknown';
try {
$stmt_log = $pdo->prepare("INSERT INTO activity_logs (user_id, action, details, ip_address) VALUES (NULL, 'LOGIN_FAILED', ?, ?)");
$stmt_log->execute(["Attempt with username: $username", $ip]);
} catch (Exception $e) {}
}
}
}
?>
<!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" type="image/png" href="assets/img/logo.png">
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<style>
body { font-family: 'Sarabun', sans-serif; }
.shiny-logo {
position: relative;
overflow: hidden;
display: inline-block;
border-radius: 50%; /* Adjust depending on logo shape */
}
.shiny-logo::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 6s infinite ease-in-out;
}
@keyframes shine {
0% { left: -150%; }
15% { left: 200%; }
100% { left: 200%; } /* Long pause before shining again */
}
</style>
<link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@300;400;500;600;700&display=swap" rel="stylesheet">
</head>
<body class="bg-gray-100 flex items-center justify-center h-screen">
<div class="bg-white p-8 rounded-lg shadow-lg w-full max-w-md">
<div class="text-center mb-8">
<div class="shiny-logo mb-4">
<img src="assets/img/logo.png" alt="Logo" class="w-24 h-24 object-contain">
</div>
<h1 class="text-2xl font-bold text-gray-800">ระบบแจ้งเตือนส่งงาน</h1>
<p class="text-gray-500 text-sm mt-2">เข้าสู่ระบบเพื่อจัดการและดูงานของคุณ</p>
</div>
<?php if ($error): ?>
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4" role="alert">
<span class="block sm:inline"><?= htmlspecialchars($error) ?></span>
</div>
<?php endif; ?>
<form method="POST" action="">
<div class="mb-4">
<label for="username" class="block text-gray-700 text-sm font-bold mb-2"><i class="fas fa-user mr-2"></i>ชื่อผู้ใช้</label>
<input type="text" id="username" name="username" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:ring-2 focus:ring-blue-500" required>
</div>
<div class="mb-6">
<label for="password" class="block text-gray-700 text-sm font-bold mb-2"><i class="fas fa-lock mr-2"></i>รหัสผ่าน</label>
<input type="password" id="password" name="password" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:ring-2 focus:ring-blue-500" required>
</div>
<div class="flex items-center justify-between">
<button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline w-full transition duration-300">
<i class="fas fa-sign-in-alt mr-2"></i> เข้าสู่ระบบ
</button>
</div>
</form>
</div>
</body>
</html>
@@ -0,0 +1,13 @@
<?php
session_start();
require_once 'config.php';
if (isLoggedIn()) {
logActivity($pdo, 'LOGOUT', "User logged out");
}
session_unset();
session_destroy();
header('Location: index.php');
exit;
?>
@@ -0,0 +1,217 @@
<?php
require_once 'config.php';
requireLogin();
$role = $_SESSION['role'];
include 'includes/header.php';
include 'includes/sidebar.php';
?>
<div class="mb-6">
<h2 class="text-2xl font-bold text-gray-800">คู่มือการใช้งานระบบ</h2>
<p class="text-gray-600 mt-1">ยินดีต้อนรับสู่คู่มือการใช้งานระบบแจ้งเตือนส่งงาน</p>
</div>
<div class="flex flex-col md:flex-row gap-6 items-start">
<!-- Table of Contents (Sidebar) -->
<div class="w-full md:w-1/4 bg-white rounded-lg shadow-sm p-4 sticky top-4 shrink-0 border border-gray-100">
<h3 class="font-bold text-gray-800 mb-3 uppercase text-sm tracking-wider border-b pb-2">สารบัญ</h3>
<nav class="flex flex-col space-y-1">
<?php if ($role === 'admin'): ?>
<a href="#overview" class="text-gray-600 hover:text-blue-600 hover:bg-blue-50 py-1 px-2 rounded text-sm transition-colors">1. ภาพรวม / Dashboard</a>
<a href="#assignments" class="text-gray-600 hover:text-blue-600 hover:bg-blue-50 py-1 px-2 rounded text-sm transition-colors">2. การจัดการงาน (Assignments)</a>
<a href="#users" class="text-gray-600 hover:text-blue-600 hover:bg-blue-50 py-1 px-2 rounded text-sm transition-colors">3. การจัดการผู้ใช้ (Users)</a>
<a href="#settings" class="text-gray-600 hover:text-blue-600 hover:bg-blue-50 py-1 px-2 rounded text-sm transition-colors">4. การตั้งค่าระบบ (Settings)</a>
<a href="#logs" class="text-gray-600 hover:text-blue-600 hover:bg-blue-50 py-1 px-2 rounded text-sm transition-colors">5. ประวัติการใช้งาน (Activity Logs)</a>
<a href="#notifications" class="text-gray-600 hover:text-blue-600 hover:bg-blue-50 py-1 px-2 rounded text-sm transition-colors">6. การเชื่อมต่อแจ้งเตือน & Cron</a>
<?php else: ?>
<a href="#student-tasks" class="text-gray-600 hover:text-blue-600 hover:bg-blue-50 py-1 px-2 rounded text-sm transition-colors">1. งานของฉัน (My Assignments)</a>
<a href="#student-alerts" class="text-gray-600 hover:text-blue-600 hover:bg-blue-50 py-1 px-2 rounded text-sm transition-colors">2. ระบบการแจ้งเตือน</a>
<a href="#responsive-ui" class="text-gray-600 hover:text-blue-600 hover:bg-blue-50 py-1 px-2 rounded text-sm transition-colors">3. การใช้งานบนมือถือ</a>
<?php endif; ?>
</nav>
</div>
<!-- Content Area -->
<div class="w-full md:w-3/4 space-y-8 pb-12">
<?php if ($role === 'admin'): ?>
<!-- Admin Section -->
<section id="overview" class="bg-white rounded-lg shadow-md p-6 scroll-mt-6 border-l-4 border-blue-500">
<h3 class="text-xl font-bold text-gray-800 mb-4 border-b pb-2 text-blue-700">1. ภาพรวมระบบ (Dashboard)</h3>
<p class="text-gray-700 leading-relaxed mb-4">
หน้า Dashboard เป็นหน้าแรกหลังจากเข้าสู่ระบบ ใช้สำหรับติดตามภาพรวมของงานทั้งหมด
</p>
<ul class="list-disc pl-6 text-sm text-gray-700 space-y-2">
<li><strong class="text-gray-900">การ์ดสรุปข้อมูล:</strong> แสดงจำนวนงานทั้งหมด, งานที่รอดำเนินการ, งานที่เสร็จสิ้น และจำนวนนักเรียน <span class="bg-yellow-100 text-yellow-800 px-1 rounded text-xs">คุณสามารถคลิกที่การ์ดเพื่อกรองข้อมูลในตารางด้านล่างได้ทันที</span></li>
<li><strong class="text-gray-900">ตารางงานใกล้กำหนดส่ง:</strong> แสดงรายการงานที่ต้องรีบติดตาม โดยเรียงลำดับจากงานที่เพิ่มล่าสุด (ลำดับ 1)</li>
<li><strong class="text-gray-900">ป้ายแจ้งเตือนวันที่เหลือ:</strong> หากใกล้กำหนดจะแสดง <span class="bg-orange-500 text-white px-2 py-0.5 rounded text-xs">สีส้ม</span> หากเลยกำหนดจะแสดง <span class="bg-gray-800 text-white px-2 py-0.5 rounded text-xs">สีดำ</span></li>
<li><strong class="text-gray-900">การส่งออกไฟล์:</strong> สามารถกดปุ่ม "ส่งออก Excel" เพื่อโหลดข้อมูลตารางไปประมวลผลต่อได้</li>
</ul>
</section>
<section id="assignments" class="bg-white rounded-lg shadow-md p-6 scroll-mt-6 border-l-4 border-indigo-500">
<h3 class="text-xl font-bold text-gray-800 mb-4 border-b pb-2 text-blue-700">2. การจัดการงาน (Assignments)</h3>
<p class="text-gray-700 leading-relaxed mb-4">
เมนูสำหรับ มอบหมายงานใหม่ แก้ไข และติดตามสถานะงานของนักเรียนแต่ละคน
</p>
<ul class="list-disc pl-6 text-sm text-gray-700 space-y-2">
<li><strong class="text-gray-900">เพิ่มงานใหม่:</strong> เลือกเมนู "จัดการงาน" จะพบฟอร์มสำหรับสร้างงาน กรอก ชื่องาน/ชื่อวิชา, กำหนดส่ง <span class="bg-green-100 text-green-800 px-1 rounded text-xs">คุณสามารถค้นหาชื่อและติ๊กเลือกนักเรียนได้หลายคนพร้อมกันในครั้งเดียว (Multiple Select)</span></li>
<li><strong class="text-gray-900">แก้ไขงาน:</strong> กดปุ่มสีเหลืองรูปดินสอเพื่อแก้ไขรายละเอียด หรือเปลี่ยนสถานะเป็น "เสร็จสิ้น" <br><span class="text-blue-600 text-xs">* พิเศษ: ในหน้าแก้ไขงาน คุณสามารถติ๊กเลือกชื่อนักเรียนคนอื่นเพิ่มได้ ระบบจะทำการโคลน (Clone) งานนี้ไปสั่งนักเรียนคนใหม่ให้ทันที</span></li>
<li><strong class="text-gray-900">ลบงาน:</strong> กดปุ่มสีแดงรูปถังขยะหากต้องการลบงานนั้นออกจากระบบ (ระบบจะลบข้อมูลออกจากฐานข้อมูลทันที)</li>
</ul>
</section>
<section id="users" class="bg-white rounded-lg shadow-md p-6 scroll-mt-6 border-l-4 border-green-500">
<h3 class="text-xl font-bold text-gray-800 mb-4 border-b pb-2 text-blue-700">3. การจัดการผู้ใช้ (Users)</h3>
<p class="text-gray-700 leading-relaxed mb-4">
เมนูสำหรับเพิ่มและจัดการบัญชีนักเรียน รวมถึงบัญชีผู้ดูแลระบบท่านอื่นๆ (รายชื่อจะถูกเรียงตามตัวอักษร ก-ฮ โดยอัตโนมัติ)
</p>
<ul class="list-disc pl-6 text-sm text-gray-700 space-y-2">
<li><strong class="text-gray-900">เพิ่มนักเรียนใหม่:</strong> ต้องกำหนด "ชื่อผู้ใช้ (Username)" และ "รหัสผ่าน" ให้นักเรียนนำไปล็อกอิน</li>
<li><strong class="text-gray-900">รีเซ็ตรหัสผ่าน:</strong> หากนักเรียนลืมรหัสผ่าน แอดมินสามารถกดแก้ไข และพิมพ์รหัสผ่านใหม่ลงไปได้เลย หากปล่อยว่างไว้จะเป็นการใช้รหัสผ่านเดิม</li>
<li><strong class="text-gray-900">การลบบัญชี:</strong> แอดมินไม่สามารถลบบัญชีของตัวเองได้ เพื่อป้องกันความผิดพลาด</li>
</ul>
</section>
<section id="settings" class="bg-white rounded-lg shadow-md p-6 scroll-mt-6 border-l-4 border-yellow-500">
<h3 class="text-xl font-bold text-gray-800 mb-4 border-b pb-2 text-blue-700">4. การตั้งค่าระบบ (Settings)</h3>
<p class="text-gray-700 leading-relaxed mb-4">
เมนูสำหรับควบคุมการทำงานทั้งหมดของระบบ แบ่งออกเป็น 2 แท็บ คือ "การตั้งค่าทั่วไป" และ "ประวัติการใช้งาน"
</p>
<ul class="list-disc pl-6 text-sm text-gray-700 space-y-2">
<li><strong class="text-gray-900">จำนวนวันแจ้งเตือน:</strong> กำหนดว่าถ้านานแค่ไหนก่อนถึงวันส่ง ให้ถือว่าเป็น "ใกล้กำหนด" (เช่น 3 วัน) ตัวอักษรจะเป็นสีส้ม</li>
<li><strong class="text-gray-900">เวลาแจ้งเตือนรายวัน:</strong> เวลาอ้างอิงสำหรับการตั้งค่า Cron Job</li>
<li><strong class="text-gray-900">ข้อความ Footer:</strong> ข้อความลิขสิทธิ์ด้านล่างสุดของเว็บ สามารถเปลี่ยนเป็นชื่อโรงเรียนหรือชื่อวิชาได้</li>
<li><strong class="text-gray-900">เปิด-ปิด API:</strong> สามารถติ๊กเลือกเปิดหรือปิดระบบแจ้งเตือนเข้ากลุ่ม LINE และ Telegram ได้</li>
<li><strong class="text-gray-900">ปุ่มทดสอบส่ง:</strong> แนะนำให้กดปุ่มนี้เสมอเมื่อตั้งค่า Token ใหม่ เพื่อทดสอบว่าบอทสามารถส่งข้อความเข้ากลุ่มได้จริง</li>
</ul>
</section>
<section id="logs" class="bg-white rounded-lg shadow-md p-6 scroll-mt-6 border-l-4 border-red-500">
<h3 class="text-xl font-bold text-gray-800 mb-4 border-b pb-2 text-blue-700">5. ประวัติการใช้งาน (Activity Logs)</h3>
<p class="text-gray-700 leading-relaxed mb-4">
แท็บที่สองในหน้าตั้งค่าระบบ ใช้สำหรับตรวจสอบความเคลื่อนไหวทั้งหมดที่เกิดขึ้นในระบบ
</p>
<ul class="list-disc pl-6 text-sm text-gray-700 space-y-2">
<li>บันทึกการ Login เข้าสู่ระบบ (รวมถึงคนที่พยายามเข้าสู่ระบบแต่รหัสผิด พร้อมบันทึก IP Address)</li>
<li>บันทึกการเพิ่ม แก้ไข หรือลบงาน และ ผู้ใช้งาน (รู้ว่าใครเป็นคนลบหรือแก้)</li>
<li>บันทึกการปรับปรุงการตั้งค่าระบบ</li>
<li>บันทึกประวัติการส่งแจ้งเตือนอัตโนมัติ (Cron) ว่าส่งไปกี่ข้อความในแต่ละวัน</li>
<li><span class="bg-gray-100 px-1 rounded border border-gray-300">รองรับการ Export ออกเป็น Excel เพื่อเก็บไว้เป็นหลักฐานย้อนหลังได้</span></li>
</ul>
</section>
<section id="notifications" class="bg-white rounded-lg shadow-md p-6 scroll-mt-6 border-l-4 border-purple-500">
<h3 class="text-xl font-bold text-gray-800 mb-4 border-b pb-2 text-blue-700">6. การเชื่อมต่อแจ้งเตือน & Cron Job</h3>
<div class="space-y-6">
<!-- LINE Notify -->
<div class="border border-green-200 rounded p-4 bg-green-50">
<h5 class="font-bold text-green-700 mb-2"><i class="fab fa-line mr-1"></i> วิธีตั้งค่า LINE Notify (แบบกลุ่ม)</h5>
<ol class="list-decimal pl-4 text-sm text-gray-700 space-y-1">
<li>เข้าไปที่เว็บ <a href="https://notify-bot.line.me/" target="_blank" class="text-blue-600 underline">notify-bot.line.me</a> แล้วเข้าสู่ระบบ</li>
<li>ไปที่ "หน้าของฉัน" (My Page) เลื่อนลงมากดปุ่ม "ออก Token" (Generate token)</li>
<li>ตั้งชื่อบอท และ <strong class="text-red-500">เลือกกลุ่มแชทของห้องเรียน</strong></li>
<li>คัดลอก Token ที่ได้ มาใส่ในหน้าตั้งค่าระบบของเว็บไซต์นี้</li>
<li>เข้าไปที่กลุ่มแชท LINE ในมือถือ แล้วเชิญบัญชีชื่อ <strong class="text-green-600">LINE Notify</strong> เข้ากลุ่ม</li>
</ol>
</div>
<!-- Telegram -->
<div class="border border-blue-200 rounded p-4 bg-blue-50">
<h5 class="font-bold text-blue-700 mb-2"><i class="fab fa-telegram-plane mr-1"></i> วิธีตั้งค่า Telegram (แบบกลุ่ม)</h5>
<ol class="list-decimal pl-4 text-sm text-gray-700 space-y-1">
<li>ในแอป Telegram ค้นหาบอทชื่อ <strong>@BotFather</strong></li>
<li>พิมพ์ <code>/newbot</code> เพื่อสร้างบอทใหม่และรับ <strong class="text-blue-600">Bot Token</strong></li>
<li>สร้างกลุ่มแชท Telegram ใหม่ แล้วดึงบอทที่คุณสร้างเข้ากลุ่ม</li>
<li>ดึงบอทชื่อ <strong>@RawDataBot</strong> เข้ากลุ่ม หาคำว่า <code>"chat": {"id": -100xxxxxxxx}</code> นำตัวเลขนั้นมาใส่ช่อง <strong class="text-blue-600">Group Chat ID</strong></li>
</ol>
</div>
</div>
<div class="mt-6 border-t pt-4">
<h4 class="font-bold text-lg text-gray-800 mb-3"><i class="fas fa-clock mr-2 text-gray-500"></i> การตั้งค่า Cron Job (แจ้งเตือนอัตโนมัติ)</h4>
<p class="text-sm text-gray-700 mb-3">
เพื่อให้ระบบทำงานส่งแจ้งเตือนทุกวันอัตโนมัติ คุณต้องตั้งค่า Cron Job ใน Hosting ของคุณ ให้เรียกไฟล์ <code class="bg-gray-100 px-1 py-0.5 rounded border border-gray-300">cron_notify.php</code>
</p>
<div class="bg-gray-800 text-white p-4 rounded font-mono text-sm overflow-x-auto">
<span class="text-gray-400"># ตัวอย่างการตั้ง Cron ให้รันทุกวัน เวลา 08:00</span><br>
0 8 * * * wget -qO- http://yourwebsite.com/cron_notify.php &gt; /dev/null 2&gt;&amp;1
</div>
<p class="text-xs text-gray-500 mt-2">* หรือกดปุ่ม "เปิดรัน Cron Job แบบ Manual" ด้านล่างของหน้าตั้งค่า เพื่อสั่งรันด้วยตัวเองก็ได้</p>
</div>
</section>
<?php else: ?>
<!-- Student Section -->
<section id="student-tasks" class="bg-white rounded-lg shadow-md p-6 scroll-mt-6 border-l-4 border-blue-500">
<h3 class="text-xl font-bold text-gray-800 mb-4 border-b pb-2 text-blue-700">1. งานของฉัน (My Assignments)</h3>
<p class="text-gray-700 leading-relaxed mb-4">
หน้านี้จะรวบรวมงานทั้งหมดที่คุณต้องส่ง โดยเรียงลำดับจากงานใหม่ล่าสุดอยู่บนสุด (ลำดับที่ 1)
</p>
<ul class="list-disc pl-6 text-sm text-gray-700 space-y-2">
<li><strong class="text-gray-900">ดูรายละเอียดงาน:</strong> กดปุ่ม "ดูรายละเอียด" เพื่อเปิดหน้าต่าง Popup อ่านคำอธิบายแบบเต็ม และ <span class="bg-blue-100 text-blue-800 px-1 rounded text-xs">ดูรายชื่อเพื่อนทั้งหมดที่ได้รับมอบหมายงานชิ้นเดียวกันนี้ได้</span></li>
<li><strong class="text-gray-900">ป้ายสีแจ้งเตือน:</strong> หากใกล้ถึงวันส่ง จะมีป้ายสีส้มบอกจำนวนวันที่เหลือ แต่หาก <span class="bg-gray-800 text-white px-2 py-0.5 rounded text-xs">ป้ายเป็นสีดำ</span> หมายความว่าคุณส่งงานล่าช้ากว่ากำหนดแล้ว</li>
<li><strong class="text-yellow-600">รอดำเนินการ:</strong> งานที่ยังไม่ได้ส่ง</li>
<li><strong class="text-green-600">เสร็จสิ้น:</strong> งานที่คุณส่งแล้วและคุณครูยืนยันในระบบแล้ว</li>
</ul>
</section>
<section id="student-alerts" class="bg-white rounded-lg shadow-md p-6 scroll-mt-6 border-l-4 border-indigo-500">
<h3 class="text-xl font-bold text-gray-800 mb-4 border-b pb-2 text-blue-700">2. ระบบการแจ้งเตือน</h3>
<p class="text-gray-700 leading-relaxed mb-4">
ระบบจะช่วยเตือนความจำให้คุณไม่พลาดการส่งงาน ผ่าน 2 ช่องทาง:
</p>
<div class="space-y-4">
<div class="flex items-start">
<i class="fas fa-window-restore text-3xl text-indigo-400 mr-4 mt-1"></i>
<div>
<strong class="block text-gray-800">แจ้งเตือนบนเว็บไซต์ (Popup)</strong>
<p class="text-sm text-gray-600">ทุกครั้งที่คุณล็อกอินเข้าสู่ระบบ หากมีงานใกล้กำหนดส่ง จะมีกล่องแจ้งเตือนเด้งขึ้นมากลางหน้าจอ **เพียง 1 ครั้งเท่านั้น** (สามารถใช้นิ้วเลื่อนดูรายการงานทั้งหมดในกล่องได้หากมีหลายงาน)</p>
</div>
</div>
<div class="flex items-start">
<i class="fas fa-mobile-alt text-3xl text-green-500 mr-4 mt-1"></i>
<div>
<strong class="block text-gray-800">แจ้งเตือนเข้าแอปพลิเคชัน</strong>
<p class="text-sm text-gray-600">ข้อความแจ้งเตือนจะถูกส่งเข้าไปในกลุ่มแชทของห้องเรียน (เช่น LINE หรือ Telegram) ในตอนเช้าของทุกวัน</p>
</div>
</div>
</div>
</section>
<section id="responsive-ui" class="bg-white rounded-lg shadow-md p-6 scroll-mt-6 border-l-4 border-green-500">
<h3 class="text-xl font-bold text-gray-800 mb-4 border-b pb-2 text-blue-700">3. การใช้งานบนมือถือ (Mobile Responsive)</h3>
<p class="text-gray-700 leading-relaxed mb-4">
เว็บไซต์ถูกออกแบบมาให้รองรับการใช้งานบนหน้าจอทุกขนาด:
</p>
<div class="flex items-start">
<i class="fas fa-plus-circle text-3xl text-green-500 bg-white mr-4 mt-1"></i>
<div>
<strong class="block text-gray-800">ปุ่มขยายข้อมูล (เครื่องหมายบวก)</strong>
<p class="text-sm text-gray-600">หากคุณเปิดเว็บด้วยโทรศัพท์มือถือ ตารางจะซ่อนข้อมูลบางส่วนอัตโนมัติเพื่อให้พอดีกับหน้าจอ คุณสามารถแตะที่ปุ่ม <strong class="text-green-600">+ (บวก)</strong> ที่อยู่หน้าแถว เพื่อกางข้อมูลส่วนที่ซ่อนอยู่ออกมาดูได้ทันที</p>
</div>
</div>
</section>
<?php endif; ?>
</div>
</div>
<script>
// Simple smooth scrolling for sidebar links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
document.querySelector(this.getAttribute('href')).scrollIntoView({
behavior: 'smooth'
});
});
});
</script>
<?php include 'includes/footer.php'; ?>
@@ -0,0 +1,145 @@
<?php
require_once 'config.php';
requireLogin();
// This page is for students
if (isAdmin()) {
header('Location: assignments.php');
exit;
}
$user_id = $_SESSION['user_id'];
include 'includes/header.php';
include 'includes/sidebar.php';
?>
<div class="mb-6 flex justify-between items-center">
<div>
<h2 class="text-2xl font-bold text-gray-800">งานของฉัน</h2>
<p class="text-gray-600 mt-1">รายการงานที่ได้รับมอบหมายทั้งหมด</p>
</div>
</div>
<div class="bg-white rounded-lg shadow-md p-6">
<table class="dataTable display w-full text-sm text-left text-gray-500">
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
<th class="text-center w-16">ลำดับ</th>
<th class="text-center">ชื่องาน/ชื่อวิชา</th>
<th class="text-center">รายละเอียด</th>
<th class="text-center w-32">วันที่เพิ่มข้อมูล</th>
<th class="text-center">กำหนดส่ง</th>
<th class="text-center w-24">สถานะ</th>
</tr>
</thead>
<tbody>
<?php
$days_before = getSetting($pdo, 'days_before_due') ?: 3;
$stmt = $pdo->prepare("SELECT *, DATEDIFF(due_date, CURDATE()) as days_left FROM assignments WHERE student_id = ? ORDER BY id DESC");
$stmt->execute([$user_id]);
$index = 1;
while ($row = $stmt->fetch()):
// Get all assigned students for this specific task
$stmt_group = $pdo->prepare("SELECT u.name FROM assignments a JOIN users u ON a.student_id = u.id WHERE a.title = ? AND a.due_date = ?");
$stmt_group->execute([$row['title'], $row['due_date']]);
$assigned_names = $stmt_group->fetchAll(PDO::FETCH_COLUMN);
$assigned_list = implode(', ', $assigned_names);
// Format Created At
$created_timestamp = strtotime($row['created_at']);
$created_date_th = date('d-m-', $created_timestamp) . (date('Y', $created_timestamp) + 543);
// Format Due Date
$due_timestamp = strtotime($row['due_date']);
$due_date_th = date('d-m-', $due_timestamp) . (date('Y', $due_timestamp) + 543);
$due_html = "<span>{$due_date_th}</span>";
$status_text = "รอดำเนินการ";
if ($row['status'] === 'pending') {
if ($row['days_left'] < 0) {
$due_html .= " <span class=\"bg-gray-800 text-white text-xs px-2 py-0.5 rounded ml-2 whitespace-nowrap\">เลยกำหนด " . abs($row['days_left']) . " วัน</span>";
} elseif ($row['days_left'] <= $days_before) {
$due_html .= " <span class=\"bg-orange-500 text-white text-xs px-2 py-0.5 rounded ml-2 whitespace-nowrap\">เหลือ {$row['days_left']} วัน</span>";
} else {
$due_html .= " <span class=\"bg-gray-100 text-gray-600 text-xs px-2 py-0.5 rounded ml-2 whitespace-nowrap\">เหลือ {$row['days_left']} วัน</span>";
}
} else {
$status_text = "เสร็จสิ้น";
}
?>
<tr class="bg-white border-b hover:bg-gray-50">
<td class="text-center"><?= $index++ ?></td>
<td class="font-medium text-gray-900"><?= htmlspecialchars($row['title']) ?></td>
<td class="text-center">
<button type="button"
onclick="showDetails(
'<?= htmlspecialchars(addslashes($row['title'])) ?>',
'<?= htmlspecialchars(addslashes($row['description'] ?: '-')) ?>',
'<?= htmlspecialchars(addslashes($assigned_list)) ?>',
'<?= $due_date_th ?>',
'<?= $status_text ?>'
)"
class="text-blue-600 hover:text-blue-800 bg-blue-50 hover:bg-blue-100 px-3 py-1 rounded-full text-xs font-semibold transition-colors">
<i class="fas fa-info-circle mr-1"></i> ดูรายละเอียด
</button>
</td>
<td class="text-center"><?= $created_date_th ?></td>
<td class="text-center"><?= $due_html ?></td>
<td class="text-center">
<?php if ($row['status'] === 'submitted' || $row['status'] === 'completed'): ?>
<span class="bg-green-100 text-green-800 py-1 px-2 rounded-full text-xs font-semibold">เสร็จสิ้น</span>
<?php else: ?>
<span class="bg-yellow-100 text-yellow-800 py-1 px-2 rounded-full text-xs font-semibold">รอดำเนินการ</span>
<?php endif; ?>
</td>
</tr>
<?php endwhile; ?>
</tbody>
</table>
</div>
<script>
function showDetails(title, desc, assignedTo, dueDate, status) {
const statusColor = status === 'เสร็จสิ้น' ? 'text-green-600' : 'text-yellow-600';
let htmlContent = `
<div class="text-left space-y-4 text-sm mt-4">
<div class="bg-gray-50 p-3 rounded border">
<p class="text-gray-500 mb-1"><i class="fas fa-align-left mr-2"></i><strong>รายละเอียดงาน:</strong></p>
<p class="text-gray-800 whitespace-pre-wrap">${desc}</p>
</div>
<div class="flex items-center justify-between border-b pb-2">
<span class="text-gray-500"><i class="fas fa-users mr-2"></i><strong>ผู้ที่ถูกมอบหมาย:</strong></span>
<span class="text-gray-800 text-right ml-4">${assignedTo}</span>
</div>
<div class="flex items-center justify-between border-b pb-2">
<span class="text-gray-500"><i class="far fa-calendar-alt mr-2"></i><strong>วันที่กำหนดส่ง:</strong></span>
<span class="text-gray-800 font-medium">${dueDate}</span>
</div>
<div class="flex items-center justify-between pb-2">
<span class="text-gray-500"><i class="fas fa-info-circle mr-2"></i><strong>สถานะปัจจุบัน:</strong></span>
<span class="font-bold ${statusColor}">${status}</span>
</div>
</div>
`;
Swal.fire({
title: `<div class="text-xl text-blue-700 font-bold border-b pb-3">${title}</div>`,
html: htmlContent,
width: 600,
showCloseButton: true,
confirmButtonText: 'ปิดหน้าต่าง',
confirmButtonColor: '#3085d6',
customClass: {
title: 'mb-0'
}
});
}
</script>
<?php include 'includes/footer.php'; ?>
@@ -0,0 +1,369 @@
<?php
require_once 'config.php';
requireLogin();
requireAdmin();
$success = '';
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$days_before = $_POST['days_before_due'] ?? '3';
$notification_time = $_POST['notification_time'] ?? '08:00';
$footer_text = $_POST['footer_text'] ?? 'ระบบแจ้งเตือนส่งงาน';
$telegram_bot_token = $_POST['telegram_bot_token'] ?? '';
$telegram_group_chat_id = $_POST['telegram_group_chat_id'] ?? '';
$line_notify_token = $_POST['line_notify_token'] ?? '';
// Checkboxes
$notify_via_line = isset($_POST['notify_via_line']) ? '1' : '0';
$notify_via_telegram = isset($_POST['notify_via_telegram']) ? '1' : '0';
try {
$pdo->beginTransaction();
$settings = [
'days_before_due' => $days_before,
'notification_time' => $notification_time,
'footer_text' => $footer_text,
'telegram_bot_token' => $telegram_bot_token,
'telegram_group_chat_id' => $telegram_group_chat_id,
'line_notify_token' => $line_notify_token,
'notify_via_line' => $notify_via_line,
'notify_via_telegram' => $notify_via_telegram
];
$stmt = $pdo->prepare("INSERT INTO settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value = ?");
foreach ($settings as $key => $val) {
$stmt->execute([$key, $val, $val]);
}
$pdo->commit();
// Exclude sensitive tokens from logs
$safe_settings = $settings;
$safe_settings['telegram_bot_token'] = !empty($safe_settings['telegram_bot_token']) ? '***' : '';
$safe_settings['line_notify_token'] = !empty($safe_settings['line_notify_token']) ? '***' : '';
logActivity($pdo, 'UPDATE_SETTINGS', json_encode($safe_settings, JSON_UNESCAPED_UNICODE));
$success = 'บันทึกการตั้งค่าสำเร็จ';
} catch (Exception $e) {
$pdo->rollBack();
$error = 'เกิดข้อผิดพลาด: ' . $e->getMessage();
}
}
$days_before_due = getSetting($pdo, 'days_before_due') ?: '3';
$notification_time = getSetting($pdo, 'notification_time') ?: '08:00';
$footer_text = getSetting($pdo, 'footer_text') ?: 'ระบบแจ้งเตือนส่งงาน';
$bot_token = getSetting($pdo, 'telegram_bot_token') ?: '';
$group_chat_id = getSetting($pdo, 'telegram_group_chat_id') ?: '';
$line_notify_token = getSetting($pdo, 'line_notify_token') ?: '';
$notify_via_line = getSetting($pdo, 'notify_via_line') === '0' ? false : true; // Default true
$notify_via_telegram = getSetting($pdo, 'notify_via_telegram') === '0' ? false : true; // Default true
include 'includes/header.php';
include 'includes/sidebar.php';
?>
<div class="mb-4">
<h2 class="text-2xl font-bold text-gray-800">ตั้งค่าระบบ</h2>
<p class="text-gray-600 mt-1">ตั้งค่าการแจ้งเตือนและการเชื่อมต่อ API</p>
</div>
<!-- Tabs Navigation -->
<div class="mb-6 border-b border-gray-200">
<nav class="-mb-px flex space-x-8" aria-label="Tabs">
<button onclick="switchTab('general')" id="tab-general" class="border-blue-500 text-blue-600 whitespace-nowrap py-3 px-1 border-b-2 font-bold text-sm transition-colors">
<i class="fas fa-cogs mr-2"></i> การตั้งค่าทั่วไป
</button>
<button onclick="switchTab('logs')" id="tab-logs" class="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-3 px-1 border-b-2 font-medium text-sm transition-colors">
<i class="fas fa-history mr-2"></i> ประวัติการใช้งาน
</button>
</nav>
</div>
<?php if ($error): ?>
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-6 rounded shadow-sm">
<p><i class="fas fa-exclamation-circle mr-2"></i> <?= htmlspecialchars($error) ?></p>
</div>
<?php endif; ?>
<?php if ($success): ?>
<script>
Swal.fire({
icon: 'success',
title: 'บันทึกสำเร็จ',
text: '<?= addslashes($success) ?>',
timer: 2000,
showConfirmButton: false
});
</script>
<?php endif; ?>
<!-- General Settings Content -->
<div id="content-general">
<form method="POST" action="" id="settingsForm">
<!-- General Settings -->
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
<h3 class="text-lg font-bold mb-4 border-b pb-2"><i class="fas fa-cogs text-gray-500 mr-2"></i> ตั้งค่าทั่วไป (General)</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-gray-700 text-sm font-bold mb-2" for="days_before_due">จำนวนวันแจ้งเตือนล่วงหน้า (วัน)</label>
<div class="flex items-center">
<input type="number" name="days_before_due" id="days_before_due" value="<?= htmlspecialchars($days_before_due) ?>" min="1" max="30" class="shadow appearance-none border rounded py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:ring-2 focus:ring-blue-500 w-24 mr-2" required>
<span class="text-gray-600 text-sm">วันก่อนถึงวันกำหนดส่ง</span>
</div>
</div>
<div>
<label class="block text-gray-700 text-sm font-bold mb-2" for="notification_time">เวลาที่จะแจ้งเตือน</label>
<input type="time" name="notification_time" id="notification_time" value="<?= htmlspecialchars($notification_time) ?>" class="shadow appearance-none border rounded py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:ring-2 focus:ring-blue-500 w-32" required>
<p class="text-xs text-gray-500 mt-1">เวลาที่ระบบจะส่งข้อความแจ้งเตือน (ใช้เป็นข้อมูลอ้างอิงในการตั้ง Cron Job)</p>
</div>
<div class="md:col-span-2">
<label class="block text-gray-700 text-sm font-bold mb-2" for="footer_text">ข้อความ Footer ด้านล่างสุด</label>
<input type="text" name="footer_text" id="footer_text" value="<?= htmlspecialchars($footer_text) ?>" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="เช่น ระบบส่งงานนักเรียน" required>
<p class="text-xs text-gray-500 mt-1">ข้อความที่จะแสดงที่แถบด้านล่างสุดของทุกหน้า (ปีปัจจุบันจะถูกเพิ่มให้อัตโนมัติ)</p>
</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<!-- LINE Settings -->
<div class="bg-white rounded-lg shadow-md p-6 border-t-4 border-green-500">
<div class="flex items-center justify-between mb-4 border-b pb-2">
<h3 class="text-lg font-bold text-green-600"><i class="fab fa-line mr-2"></i> LINE Notify</h3>
<label class="inline-flex items-center cursor-pointer">
<input type="checkbox" name="notify_via_line" value="1" class="sr-only peer" <?= $notify_via_line ? 'checked' : '' ?>>
<div class="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-green-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-green-600"></div>
<span class="ml-3 text-sm font-medium text-gray-700">เปิดใช้งาน</span>
</label>
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="line_notify_token">LINE Notify Token</label>
<input type="password" name="line_notify_token" id="line_notify_token" value="<?= htmlspecialchars($line_notify_token) ?>" placeholder="ออก Token จาก notify-bot.line.me" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:ring-2 focus:ring-green-500">
<p class="text-xs text-gray-500 mt-1">ใช้สำหรับส่งแจ้งเตือนเข้ากลุ่ม LINE</p>
</div>
<div class="mt-4 flex space-x-2">
<button type="button" onclick="testLine()" class="bg-gray-100 hover:bg-gray-200 text-gray-800 font-semibold py-2 px-4 border border-gray-300 rounded shadow-sm text-sm flex-1">
<i class="fas fa-paper-plane text-green-500 mr-1"></i> ทดสอบส่ง
</button>
</div>
<div class="mt-6 bg-gray-50 p-3 rounded text-sm text-gray-600">
<strong>วิธีใช้งาน:</strong>
<ol class="list-decimal pl-4 mt-1">
<li>ไปที่ <a href="https://notify-bot.line.me/" target="_blank" class="text-blue-500 hover:underline">notify-bot.line.me</a></li>
<li>ออก Token โดยเลือกกลุ่มที่ต้องการ</li>
<li>เชิญ "LINE Notify" เข้ากลุ่มนั้น</li>
</ol>
</div>
</div>
<!-- Telegram Settings -->
<div class="bg-white rounded-lg shadow-md p-6 border-t-4 border-blue-500">
<div class="flex items-center justify-between mb-4 border-b pb-2">
<h3 class="text-lg font-bold text-blue-600"><i class="fab fa-telegram-plane mr-2"></i> Telegram Bot</h3>
<label class="inline-flex items-center cursor-pointer">
<input type="checkbox" name="notify_via_telegram" value="1" class="sr-only peer" <?= $notify_via_telegram ? 'checked' : '' ?>>
<div class="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
<span class="ml-3 text-sm font-medium text-gray-700">เปิดใช้งาน</span>
</label>
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="telegram_bot_token">Telegram Bot Token</label>
<input type="password" name="telegram_bot_token" id="telegram_bot_token" value="<?= htmlspecialchars($bot_token) ?>" placeholder="123456:ABC-DEF1234gh..." class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:ring-2 focus:ring-blue-500 mb-3">
<label class="block text-gray-700 text-sm font-bold mb-2" for="telegram_group_chat_id">Telegram Group Chat ID</label>
<input type="text" name="telegram_group_chat_id" id="telegram_group_chat_id" value="<?= htmlspecialchars($group_chat_id) ?>" placeholder="-1001234567890" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div class="mt-4 flex space-x-2">
<button type="button" onclick="testTelegram()" class="bg-gray-100 hover:bg-gray-200 text-gray-800 font-semibold py-2 px-4 border border-gray-300 rounded shadow-sm text-sm flex-1">
<i class="fas fa-paper-plane text-blue-500 mr-1"></i> ทดสอบส่ง
</button>
</div>
<div class="mt-6 bg-gray-50 p-3 rounded text-sm text-gray-600">
<strong>วิธีใช้งาน:</strong>
<ol class="list-decimal pl-4 mt-1">
<li>สร้าง Bot กับ <a href="https://t.me/botfather" target="_blank" class="text-blue-500 hover:underline">@BotFather</a></li>
<li>ดึง Bot เข้ากลุ่มที่ต้องการแจ้งเตือน</li>
<li>หา Chat ID ของกลุ่ม (ใช้ @RawDataBot ช่วยหาได้)</li>
</ol>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow-md p-4 flex justify-between items-center sticky bottom-4 border border-gray-200">
<div>
<a href="cron_notify.php" target="_blank" class="text-blue-600 hover:text-blue-800 text-sm font-medium">
<i class="fas fa-external-link-alt mr-1"></i> เปิดรัน Cron Job แบบ Manual
</a>
</div>
<button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-6 rounded shadow-lg flex items-center transition duration-200">
<i class="fas fa-save mr-2"></i> บันทึกการตั้งค่าทั้งหมด
</button>
</div>
</form>
<script>
function testLine() {
const token = document.getElementById('line_notify_token').value;
if (!token) {
Swal.fire('ข้อผิดพลาด', 'กรุณากรอก LINE Notify Token', 'error');
return;
}
Swal.fire({
title: 'กำลังส่ง...',
allowOutsideClick: false,
didOpen: () => { Swal.showLoading(); }
});
$.ajax({
url: 'api/test_notify.php',
method: 'POST',
data: { type: 'line', token: token },
dataType: 'json',
success: function(res) {
Swal.fire(res.status === 'success' ? 'สำเร็จ' : 'ล้มเหลว', res.message, res.status);
},
error: function(xhr, status, error) {
console.error(xhr.responseText);
let errMsg = 'ไม่สามารถเชื่อมต่อเซิร์ฟเวอร์ได้';
if (xhr.responseText) {
// Try to extract some readable error if possible, or just show a portion of it
errMsg += '<br><br><span class="text-xs text-red-500">' + xhr.responseText.substring(0, 200) + '...</span>';
}
Swal.fire({
title: 'ข้อผิดพลาด',
html: errMsg,
icon: 'error'
});
}
});
}
function testTelegram() {
const bot_token = document.getElementById('telegram_bot_token').value;
const chat_id = document.getElementById('telegram_group_chat_id').value;
if (!bot_token || !chat_id) {
Swal.fire('ข้อผิดพลาด', 'กรุณากรอกทั้ง Bot Token และ Chat ID', 'error');
return;
}
Swal.fire({
title: 'กำลังส่ง...',
allowOutsideClick: false,
didOpen: () => { Swal.showLoading(); }
});
$.ajax({
url: 'api/test_notify.php',
method: 'POST',
data: { type: 'telegram', bot_token: bot_token, chat_id: chat_id },
dataType: 'json',
success: function(res) {
Swal.fire(res.status === 'success' ? 'สำเร็จ' : 'ล้มเหลว', res.message, res.status);
},
error: function(xhr, status, error) {
console.error(xhr.responseText);
let errMsg = 'ไม่สามารถเชื่อมต่อเซิร์ฟเวอร์ได้';
if (xhr.responseText) {
errMsg += '<br><br><span class="text-xs text-red-500 text-left overflow-hidden">' + xhr.responseText.substring(0, 200) + '...</span>';
}
Swal.fire({
title: 'ข้อผิดพลาด',
html: errMsg,
icon: 'error'
});
}
});
}
function switchTab(tab) {
// Hide all contents
document.getElementById('content-general').classList.add('hidden');
document.getElementById('content-logs').classList.add('hidden');
// Reset all tabs styling
document.getElementById('tab-general').className = "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-3 px-1 border-b-2 font-medium text-sm transition-colors";
document.getElementById('tab-logs').className = "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-3 px-1 border-b-2 font-medium text-sm transition-colors";
// Show active content and style active tab
document.getElementById('content-' + tab).classList.remove('hidden');
document.getElementById('tab-' + tab).className = "border-blue-500 text-blue-600 whitespace-nowrap py-3 px-1 border-b-2 font-bold text-sm transition-colors";
}
</script>
</div> <!-- End content-general -->
<div id="content-logs" class="hidden">
<!-- Activity Logs Section -->
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
<h3 class="text-lg font-bold mb-4 border-b pb-2"><i class="fas fa-history text-gray-500 mr-2"></i> ประวัติการใช้งานระบบ (Activity Logs)</h3>
<table id="logsTable" class="dataTable display min-w-full bg-white w-full">
<thead class="bg-gray-100 text-gray-600">
<tr>
<th class="py-3 px-4 text-center font-semibold text-sm border-b w-32">วันเวลา</th>
<th class="py-3 px-4 text-center font-semibold text-sm border-b w-48">ผู้ใช้งาน</th>
<th class="py-3 px-4 text-center font-semibold text-sm border-b w-48">การกระทำ (Action)</th>
<th class="py-3 px-4 text-center font-semibold text-sm border-b">รายละเอียด (Details)</th>
<th class="py-3 px-4 text-center font-semibold text-sm border-b w-32">IP Address</th>
</tr>
</thead>
<tbody class="text-gray-700">
<!-- Data will be loaded via AJAX -->
</tbody>
</table>
</div>
<!-- DataTables Export Plugins for Logs -->
<script src="https://cdn.datatables.net/buttons/2.4.1/js/dataTables.buttons.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<script src="https://cdn.datatables.net/buttons/2.4.1/js/buttons.html5.min.js"></script>
<script>
$(document).ready(function() {
$('#logsTable').DataTable({
ajax: 'api/get_logs_table.php',
columns: [
{ data: 'created_at' },
{ data: 'user' },
{ data: 'action' },
{ data: 'details' },
{ data: 'ip_address' }
],
order: [[0, 'desc']], // Sort by date descending
scrollX: true,
columnDefs: [{ className: "dt-head-center", targets: "_all" }],
pageLength: 10,
dom: '<"flex justify-between items-center mb-4"lBf>rt<"flex justify-between items-center mt-4"ip>',
buttons: [
{
extend: 'excelHtml5',
text: '<i class="fas fa-file-excel mr-1"></i> ส่งออก Excel',
className: 'bg-green-600 text-white hover:bg-green-700 px-3 py-1 rounded text-sm',
title: 'ประวัติการใช้งานระบบ',
exportOptions: {
columns: ':visible'
}
}
],
language: {
url: '//cdn.datatables.net/plug-ins/1.13.6/i18n/th.json'
}
});
});
</script>
</div>
<?php include 'includes/footer.php'; ?>
@@ -0,0 +1,244 @@
<?php
require_once 'config.php';
requireLogin();
requireAdmin();
$action = $_GET['action'] ?? 'list';
$error = '';
$success = '';
// Handle form submissions
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = $_POST['username'] ?? '';
$name = $_POST['name'] ?? '';
$role = $_POST['role'] ?? 'student';
$line_token = $_POST['line_token'] ?? null;
$telegram_chat_id = $_POST['telegram_chat_id'] ?? null;
if (isset($_POST['add'])) {
$password = $_POST['password'] ?? '';
if (empty($username) || empty($password) || empty($name)) {
$error = 'กรุณากรอกข้อมูลที่จำเป็นให้ครบถ้วน';
} else {
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
try {
$stmt = $pdo->prepare("INSERT INTO users (username, password, role, name, line_token, telegram_chat_id) VALUES (?, ?, ?, ?, ?, ?)");
$stmt->execute([$username, $hashed_password, $role, $name, $line_token, $telegram_chat_id]);
$new_id = $pdo->lastInsertId();
logActivity($pdo, 'CREATE_USER', "Created user ID: $new_id, Username: $username, Role: $role");
$success = 'เพิ่มผู้ใช้สำเร็จ';
$action = 'list';
} catch (PDOException $e) {
if ($e->getCode() == 23000) {
$error = 'ชื่อผู้ใช้นี้มีอยู่ในระบบแล้ว';
} else {
$error = 'เกิดข้อผิดพลาด: ' . $e->getMessage();
}
}
}
} elseif (isset($_POST['edit'])) {
$id = $_POST['id'] ?? '';
$password = $_POST['password'] ?? '';
try {
if (!empty($password)) {
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
$stmt = $pdo->prepare("UPDATE users SET name=?, role=?, line_token=?, telegram_chat_id=?, password=? WHERE id=?");
$stmt->execute([$name, $role, $line_token, $telegram_chat_id, $hashed_password, $id]);
logActivity($pdo, 'UPDATE_USER', "Updated user ID: $id (including password change)");
} else {
$stmt = $pdo->prepare("UPDATE users SET name=?, role=?, line_token=?, telegram_chat_id=? WHERE id=?");
$stmt->execute([$name, $role, $line_token, $telegram_chat_id, $id]);
logActivity($pdo, 'UPDATE_USER', "Updated user ID: $id (without password change)");
}
$success = 'แก้ไขข้อมูลผู้ใช้สำเร็จ';
$action = 'list';
} catch (PDOException $e) {
$error = 'เกิดข้อผิดพลาด: ' . $e->getMessage();
}
}
}
// Handle delete
if (isset($_GET['delete'])) {
$id = $_GET['delete'];
// Prevent deleting self
if ($id == $_SESSION['user_id']) {
$error = 'ไม่สามารถลบบัญชีของตัวเองได้';
} else {
$stmt_info = $pdo->prepare("SELECT username FROM users WHERE id=?");
$stmt_info->execute([$id]);
$user_info = $stmt_info->fetch();
$stmt = $pdo->prepare("DELETE FROM users WHERE id=?");
if ($stmt->execute([$id])) {
$username = $user_info ? $user_info['username'] : 'Unknown';
logActivity($pdo, 'DELETE_USER', "Deleted user ID: $id, Username: $username");
$success = 'ลบผู้ใช้สำเร็จ';
}
}
}
include 'includes/header.php';
include 'includes/sidebar.php';
?>
<div class="mb-6 flex justify-between items-center">
<div>
<h2 class="text-2xl font-bold text-gray-800">จัดการผู้ใช้</h2>
<p class="text-gray-600 mt-1">เพิ่ม แก้ไข ลบข้อมูลนักเรียนและตั้งค่า Token</p>
</div>
<?php if ($action === 'list'): ?>
<a href="?action=add" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded shadow">
<i class="fas fa-plus mr-2"></i> เพิ่มผู้ใช้ใหม่
</a>
<?php else: ?>
<a href="users.php" class="bg-gray-500 hover:bg-gray-600 text-white font-bold py-2 px-4 rounded shadow">
<i class="fas fa-arrow-left mr-2"></i> กลับ
</a>
<?php endif; ?>
</div>
<?php if ($error): ?>
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-6 rounded shadow-sm">
<p><i class="fas fa-exclamation-circle mr-2"></i> <?= htmlspecialchars($error) ?></p>
</div>
<?php endif; ?>
<?php if ($success): ?>
<script>
Swal.fire({
icon: 'success',
title: 'สำเร็จ',
text: '<?= addslashes($success) ?>',
timer: 2000,
showConfirmButton: false
});
</script>
<?php endif; ?>
<?php if ($action === 'list'): ?>
<div class="bg-white rounded-lg shadow-md p-6">
<table class="dataTable display w-full text-sm text-left text-gray-500">
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
<th class="text-center w-16">ลำดับ</th>
<th class="text-center">ชื่อ-สกุล</th>
<th class="text-center">ชื่อผู้ใช้</th>
<th class="text-center">สิทธิ์</th>
<th class="text-center">จัดการ</th>
</tr>
</thead>
<tbody>
<?php
$stmt = $pdo->query("SELECT * FROM users ORDER BY name ASC");
$index = 1;
while ($row = $stmt->fetch()):
?>
<tr class="bg-white border-b hover:bg-gray-50">
<td class="text-center"><?= $index++ ?></td>
<td class="font-medium text-gray-900">
<div class="flex items-center">
<img class="w-8 h-8 rounded-full mr-2" src="https://ui-avatars.com/api/?name=<?= urlencode($row['name']) ?>&background=random" alt="Avatar">
<?= htmlspecialchars($row['name']) ?>
</div>
</td>
<td><?= htmlspecialchars($row['username']) ?></td>
<td class="text-center">
<?php if ($row['role'] === 'admin'): ?>
<span class="bg-purple-100 text-purple-800 text-xs font-medium px-2.5 py-0.5 rounded border border-purple-400">Admin</span>
<?php else: ?>
<span class="bg-blue-100 text-blue-800 text-xs font-medium px-2.5 py-0.5 rounded border border-blue-400">Student</span>
<?php endif; ?>
</td>
<td class="text-center">
<a href="?action=edit&id=<?= $row['id'] ?>" class="text-white bg-yellow-400 hover:bg-yellow-500 font-medium rounded-lg text-sm px-3 py-1 mr-2"><i class="fas fa-edit"></i></a>
<?php if ($row['id'] != $_SESSION['user_id']): ?>
<button onclick="confirmDelete(<?= $row['id'] ?>)" class="text-white bg-red-600 hover:bg-red-700 font-medium rounded-lg text-sm px-3 py-1"><i class="fas fa-trash"></i></button>
<?php endif; ?>
</td>
</tr>
<?php endwhile; ?>
</tbody>
</table>
</div>
<script>
function confirmDelete(id) {
Swal.fire({
title: 'ยืนยันการลบ?',
text: "งานที่เกี่ยวข้องกับผู้ใช้นี้จะถูกลบไปด้วย!",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
confirmButtonText: 'ใช่, ลบเลย!',
cancelButtonText: 'ยกเลิก'
}).then((result) => {
if (result.isConfirmed) {
window.location.href = `users.php?delete=${id}`;
}
})
}
</script>
<?php elseif ($action === 'add' || $action === 'edit'):
$editMode = ($action === 'edit' && isset($_GET['id']));
$item = null;
if ($editMode) {
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$_GET['id']]);
$item = $stmt->fetch();
if (!$item) {
echo "<script>window.location.href='users.php';</script>";
exit;
}
}
?>
<div class="bg-white rounded-lg shadow-md p-6 max-w-2xl mx-auto">
<h3 class="text-xl font-bold mb-4 border-b pb-2"><?= $editMode ? 'แก้ไขข้อมูลผู้ใช้' : 'เพิ่มผู้ใช้ใหม่' ?></h3>
<form method="POST" action="users.php">
<?php if ($editMode): ?>
<input type="hidden" name="id" value="<?= $item['id'] ?>">
<?php endif; ?>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label class="block text-gray-700 text-sm font-bold mb-2" for="username">ชื่อผู้ใช้ (Username) <span class="text-red-500">*</span></label>
<input type="text" name="username" id="username" value="<?= $editMode ? htmlspecialchars($item['username']) : '' ?>" <?= $editMode ? 'readonly class="bg-gray-100 shadow appearance-none border rounded w-full py-2 px-3 text-gray-500 leading-tight focus:outline-none"' : 'class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:ring-2 focus:ring-blue-500" required' ?>>
</div>
<div>
<label class="block text-gray-700 text-sm font-bold mb-2" for="password"><?= $editMode ? 'รหัสผ่าน (เว้นว่างถ้าไม่ต้องการเปลี่ยน)' : 'รหัสผ่าน <span class="text-red-500">*</span>' ?></label>
<input type="password" name="password" id="password" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:ring-2 focus:ring-blue-500" <?= $editMode ? '' : 'required' ?>>
</div>
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="name">ชื่อ-นามสกุล <span class="text-red-500">*</span></label>
<input type="text" name="name" id="name" value="<?= $editMode ? htmlspecialchars($item['name']) : '' ?>" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:ring-2 focus:ring-blue-500" required>
</div>
<div class="mb-6">
<label class="block text-gray-700 text-sm font-bold mb-2" for="role">สิทธิ์การใช้งาน <span class="text-red-500">*</span></label>
<select name="role" id="role" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:ring-2 focus:ring-blue-500" required>
<option value="student" <?= ($editMode && $item['role'] === 'student') ? 'selected' : '' ?>>Student (นักเรียน)</option>
<option value="admin" <?= ($editMode && $item['role'] === 'admin') ? 'selected' : '' ?>>Admin (ผู้ดูแลระบบ)</option>
</select>
</div>
<h4 class="font-bold text-gray-700 mb-2 mt-4 border-b pb-1"><i class="fas fa-bell text-yellow-500 mr-2"></i> ตั้งค่าการแจ้งเตือน</h4>
<p class="text-sm text-gray-500 mb-4">* ระบบจะทำการแจ้งเตือนไปยังกลุ่ม Telegram ที่ตั้งค่าไว้ในหน้าตั้งค่าระบบโดยอัตโนมัติ</p>
<div class="flex items-center justify-end border-t pt-4 mt-6">
<a href="users.php" class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded mr-2">ยกเลิก</a>
<button type="submit" name="<?= $editMode ? 'edit' : 'add' ?>" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
<?= $editMode ? 'บันทึกการแก้ไข' : 'เพิ่มผู้ใช้' ?>
</button>
</div>
</form>
</div>
<?php endif; ?>
<?php include 'includes/footer.php'; ?>