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,126 @@
<?php
// config/Dispatcher.php
require_once dirname(__DIR__) . '/config/database.php';
class Dispatcher {
private $conn;
public function __construct() {
$this->conn = Database::getInstance();
}
/**
* ค้นหาและจ่ายงานให้เจ้าหน้าที่โดยอัตโนมัติ (Smart Auto Dispatch)
* ปัจจัยการประเมิน (Scoring Algorithm):
* 1. สถานะ: ว่าง (Online) ได้คะแนนสูงสุด, กำลังรับส่ง (Busy) ถูกหักคะแนน
* 2. ภาระงาน: เจ้าหน้าที่ที่มีงานน้อยที่สุดในวันนี้ จะได้คะแนนบวกเพิ่ม
* 3. ระยะทาง: คำนวณจากพิกัด (user_locations) หาคนที่อยู่ใกล้ที่สุด (Haversine formula)
* @param int $job_id
* @param float $dest_lat พิกัดจุดรับ (ถ้ามี)
* @param float $dest_lng พิกัดจุดรับ (ถ้ามี)
*/
public function autoDispatch($job_id, $dest_lat = null, $dest_lng = null) {
// ดึงการตั้งค่า
require_once dirname(__DIR__) . '/models/Setting.php';
$settingModel = new Setting();
$max_distance_str = $settingModel->get('dispatch_max_distance', '2.0');
$max_distance = floatval($max_distance_str);
// ดึงรายการเจ้าหน้าที่ทั้งหมดที่ไม่ได้ออฟไลน์ พร้อมภาระงานในวันนี้ และพิกัดล่าสุด
$query = "
SELECT u.id, u.username, u.status,
(SELECT COUNT(*) FROM jobs j WHERE j.requester_id = u.id AND DATE(j.created_at) = CURDATE()) as today_jobs,
l.latitude, l.longitude
FROM users u
LEFT JOIN user_locations l ON u.id = l.user_id
AND l.last_updated >= NOW() - INTERVAL 15 MINUTE
WHERE u.role IN ('staff', 'nurse') AND u.status != 'offline'
";
$stmt = $this->conn->prepare($query);
$stmt->execute();
$staffs = $stmt->fetchAll();
if (empty($staffs)) {
return false; // ไม่มีเจ้าหน้าที่พร้อมรับงาน
}
$best_staff_id = null;
$highest_score = -9999;
foreach ($staffs as $staff) {
$score = 0;
// 1. ประเมินจากสถานะ (Status Score)
if ($staff['status'] === 'online') {
$score += 100; // ว่าง
} else {
$score += 20; // ไม่ว่าง (อาจจะรับงานซ้อนได้ถ้าระบบอนุญาต)
}
// 2. ประเมินจากภาระงาน (Workload Score)
// ยิ่งงานน้อย ยิ่งได้คะแนนเยอะ (ลดหลั่นลงไป งานละ -5 คะแนน)
$score -= ($staff['today_jobs'] * 5);
// 3. ประเมินจากระยะทาง (Distance Score)
if ($dest_lat !== null && $dest_lng !== null && $staff['latitude'] !== null && $staff['longitude'] !== null) {
$distance = $this->calculateDistance($staff['latitude'], $staff['longitude'], $dest_lat, $dest_lng);
// หากระยะทางเกินกว่าที่กำหนด จะถูกหักคะแนนหนัก หรือข้ามไปเลย
if ($distance > $max_distance) {
$score -= 100; // ตัดสิทธิ์กลายๆ
} else {
// ยิ่งใกล้ ยิ่งได้คะแนนเยอะ (สมมติว่ารัศมี 1km ได้ 50 คะแนน, หักลบตามระยะทาง)
$distance_score = max(0, 50 - ($distance * (50 / $max_distance)));
$score += $distance_score;
}
}
// ค้นหาผู้ที่ได้คะแนนสูงสุด
if ($score > $highest_score) {
$highest_score = $score;
$best_staff_id = $staff['id'];
}
}
if ($best_staff_id) {
$this->assignJob($job_id, $best_staff_id);
return $best_staff_id;
}
return false;
}
/**
* คำนวณระยะทางแบบ Haversine Formula (กิโลเมตร)
*/
private function calculateDistance($lat1, $lon1, $lat2, $lon2) {
$earth_radius = 6371; // km
$dLat = deg2rad($lat2 - $lat1);
$dLon = deg2rad($lon2 - $lon1);
$a = sin($dLat/2) * sin($dLat/2) + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLon/2) * sin($dLon/2);
$c = 2 * asin(sqrt($a));
return $earth_radius * $c;
}
private function assignJob($job_id, $staff_id) {
// อัปเดตตาราง jobs
$query = "UPDATE jobs SET requester_id = :staff_id WHERE id = :job_id";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':staff_id', $staff_id);
$stmt->bindParam(':job_id', $job_id);
$stmt->execute();
// เปลี่ยนสถานะเจ้าหน้าที่เป็นกำลังทำงาน
$update_status = "UPDATE users SET status = 'busy' WHERE id = :staff_id";
$stmt3 = $this->conn->prepare($update_status);
$stmt3->bindParam(':staff_id', $staff_id);
$stmt3->execute();
// บันทึก Timeline
$query_timeline = "INSERT INTO job_timelines (job_id, status, description, created_by) VALUES (:job_id, 'assigned', 'Smart Auto Dispatch จ่ายงานอัตโนมัติ', 0)";
$stmt2 = $this->conn->prepare($query_timeline);
$stmt2->bindParam(':job_id', $job_id);
$stmt2->execute();
}
}
@@ -0,0 +1,41 @@
<?php
// config/LineBot.php
require_once __DIR__ . '/config.php';
require_once dirname(__DIR__) . '/models/Setting.php';
class LineBot {
public function sendGroupMessage($message, $jobId = null) {
$settingModel = new Setting();
$token = $settingModel->get('line_bot_token', '');
if (empty($token)) {
return false; // Skip if token is not set
}
$text = $message;
if ($jobId) {
$acceptUrl = BASE_URL . "?page=staff&action=accept&job_id=" . $jobId;
$text .= "\n\n🔗 กดรับงาน: " . $acceptUrl;
}
$queryData = http_build_query([
'message' => "\n" . $text
]);
// LINE Notify API
$ch = curl_init('https://notify-api.line.me/api/notify');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $queryData);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Content-Type: application/x-www-form-urlencoded',
'Authorization: Bearer ' . $token
));
$result = curl_exec($ch);
curl_close($ch);
return $result;
}
}
@@ -0,0 +1,42 @@
<?php
// config/TelegramBot.php
require_once __DIR__ . '/config.php';
require_once dirname(__DIR__) . '/models/Setting.php';
class TelegramBot {
public function sendGroupMessage($message, $jobId = null) {
$settingModel = new Setting();
$token = $settingModel->get('telegram_bot_token', '');
$chat_id = $settingModel->get('telegram_chat_id', '');
if (empty($token) || empty($chat_id)) {
return false;
}
$text = $message;
if ($jobId) {
$acceptUrl = BASE_URL . "?page=staff&action=accept&job_id=" . $jobId;
$text .= "\n\n🔗 กดรับงาน: " . $acceptUrl;
}
// Actual cURL request to Telegram API
$url = "https://api.telegram.org/bot" . $token . "/sendMessage";
$data = [
'chat_id' => $chat_id,
'text' => $text,
'parse_mode' => 'HTML'
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($ch);
curl_close($ch);
return $result;
}
}
@@ -0,0 +1,44 @@
<?php
// config/config.php
session_start();
// Define base URL for the application
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://";
$host = $_SERVER['HTTP_HOST'];
// Assuming the project is in a folder named after the project in htdocs, we dynamically get the path
$script_path = dirname($_SERVER['SCRIPT_NAME']);
$base_url = $protocol . $host . $script_path . '/';
define('BASE_URL', $base_url);
// Notification API Tokens (Replace with real tokens when deploying)
define('LINE_BOT_TOKEN', 'YOUR_LINE_ACCESS_TOKEN');
define('LINE_GROUP_ID', 'YOUR_GROUP_ID');
define('TELEGRAM_BOT_TOKEN', 'YOUR_TELEGRAM_BOT_TOKEN');
define('TELEGRAM_CHAT_ID', 'YOUR_CHAT_ID');
// Security helpers
function escape($html) {
return htmlspecialchars($html, ENT_QUOTES | ENT_SUBSTITUTE, "UTF-8");
}
function generateCSRFToken() {
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
function checkCSRFToken($token) {
if (empty($_SESSION['csrf_token']) || $token !== $_SESSION['csrf_token']) {
die("CSRF Token validation failed.");
}
return true;
}
// Redirect helper
function redirect($url) {
header("Location: " . BASE_URL . $url);
exit;
}
@@ -0,0 +1,57 @@
<?php
// config/database.php
define('DB_HOST', 'localhost');
define('DB_NAME', 'ksh_porter');
define('DB_USER', 'root');
define('DB_PASS', '@Samui@10742');
// HR Database Configuration
define('HOS_DB_HOST', '10.0.250.115');
define('HOS_DB_NAME', 'hosoffice_2566');
define('HOS_DB_USER', 'hosoffice');
define('HOS_DB_PASS', 'hosoffice10742');
class Database
{
private static $instance = null;
private $conn;
private function __construct()
{
try {
$dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
$this->conn = new PDO($dsn, DB_USER, DB_PASS, $options);
} catch (PDOException $e) {
die("Database connection failed: " . $e->getMessage());
}
}
public static function getInstance()
{
if (!self::$instance) {
self::$instance = new Database();
}
return self::$instance->conn;
}
public static function getHosInstance()
{
try {
$dsn = "mysql:host=" . HOS_DB_HOST . ";dbname=" . HOS_DB_NAME . ";charset=utf8mb4";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
return new PDO($dsn, HOS_DB_USER, HOS_DB_PASS, $options);
} catch (PDOException $e) {
return null; // Return null if unable to connect to HR DB
}
}
}