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,92 @@
<?php
// models/Department.php
require_once dirname(__DIR__) . '/config/database.php';
class Department {
private $conn;
private $table_name = "departments";
public function __construct() {
$this->conn = Database::getInstance();
$this->createTableIfNotExists();
}
private function createTableIfNotExists() {
$query = "CREATE TABLE IF NOT EXISTS `" . $this->table_name . "` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`code` varchar(50) NOT NULL UNIQUE,
`name` varchar(255) NOT NULL,
`status` enum('active','inactive') DEFAULT 'active',
`building` varchar(255) DEFAULT NULL,
`floor` varchar(50) DEFAULT NULL,
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
$this->conn->exec($query);
// Ensure status column exists for older database versions
try {
$this->conn->exec("ALTER TABLE `" . $this->table_name . "` ADD COLUMN `status` ENUM('active','inactive') DEFAULT 'active'");
} catch (PDOException $e) {
// Column already exists, ignore
}
// Add default data if empty
$stmt = $this->conn->query("SELECT COUNT(*) FROM " . $this->table_name);
if ($stmt->fetchColumn() == 0) {
$this->conn->exec("INSERT INTO `" . $this->table_name . "` (`name`) VALUES ('ER (ฉุกเฉิน)'), ('OPD1 (อายุรกรรม)'), ('IPD1 (ผู้ป่วยในชาย)'), ('X-Ray (รังสีวิทยา)'), ('OR (ห้องผ่าตัด)')");
}
}
public function getAllActive() {
$query = "SELECT * FROM " . $this->table_name . " WHERE status = 'active' ORDER BY name ASC";
$stmt = $this->conn->prepare($query);
$stmt->execute();
return $stmt->fetchAll();
}
public function getById($id) {
if (!$id) return null;
$query = "SELECT * FROM " . $this->table_name . " WHERE id = :id LIMIT 1";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':id', $id);
$stmt->execute();
if ($stmt->rowCount() > 0) {
return $stmt->fetch(PDO::FETCH_ASSOC);
}
return null;
}
public function getAll() {
$query = "SELECT * FROM " . $this->table_name . " ORDER BY name ASC";
$stmt = $this->conn->prepare($query);
$stmt->execute();
return $stmt->fetchAll();
}
public function add($name) {
$code = 'D' . strtoupper(substr(uniqid(), -5)); // Generate random code
$query = "INSERT INTO " . $this->table_name . " (code, name, status) VALUES (:code, :name, 'active')";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':code', $code);
$stmt->bindParam(':name', $name);
return $stmt->execute();
}
public function update($id, $name, $status) {
$query = "UPDATE " . $this->table_name . " SET name = :name, status = :status WHERE id = :id";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':name', $name);
$stmt->bindParam(':status', $status);
$stmt->bindParam(':id', $id);
return $stmt->execute();
}
public function delete($id) {
$query = "DELETE FROM " . $this->table_name . " WHERE id = :id";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':id', $id);
return $stmt->execute();
}
}
@@ -0,0 +1,72 @@
<?php
// models/DispatchEngine.php
require_once dirname(__DIR__) . '/config/database.php';
require_once __DIR__ . '/User.php';
require_once __DIR__ . '/Job.php';
require_once __DIR__ . '/Setting.php';
require_once __DIR__ . '/Notification.php';
class DispatchEngine {
private $conn;
private $settingModel;
private $userModel;
private $jobModel;
public function __construct() {
$this->conn = Database::getInstance();
$this->settingModel = new Setting();
$this->userModel = new User();
$this->jobModel = new Job();
}
/**
* Run auto-dispatch for a newly created job
* @param int $job_id
* @return bool True if successfully assigned, false if no one available
*/
public function runAutoDispatch($job_id) {
// 1. Check if auto dispatch is enabled
$dispatchSettings = $this->settingModel->getAllFlat();
if (!isset($dispatchSettings['auto_dispatch_enabled']) || $dispatchSettings['auto_dispatch_enabled'] != '1') {
return false;
}
// 2. Find available staff
$staff = $this->userModel->getAvailableStaff();
if (!$staff) {
// No one is available, leave job as pending
return false;
}
$staff_id = $staff['id'];
$staff_name = $staff['full_name'];
// 3. Assign the job
$query = "UPDATE jobs SET status = 'assigned', assigned_to = :staff_id WHERE id = :job_id";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':staff_id', $staff_id);
$stmt->bindParam(':job_id', $job_id);
if ($stmt->execute()) {
// Log timeline
$this->jobModel->logTimeline($job_id, 'assigned', "ระบบจ่ายงานอัตโนมัติไปยัง: " . $staff_name, null);
// Note: Notification will be sent later in the job lifecycle if needed,
// but we can also trigger a specific "Auto Assigned" notification here.
try {
$job = $this->jobModel->getById($job_id);
if ($job) {
$notification = new Notification();
$msg = "🤖 [Auto Dispatch]\nระบบมอบหมายงาน " . $job['job_number'] . "\nให้กับ: " . $staff_name;
$notification->sendToAll($msg);
}
} catch (Exception $e) {
// Ignore
}
return true;
}
return false;
}
}
@@ -0,0 +1,102 @@
<?php
// models/HospitalMap.php
require_once dirname(__DIR__) . '/config/database.php';
class HospitalMap {
private $conn;
private $table_name = "hospital_maps";
public function __construct() {
$this->conn = Database::getInstance();
}
public function getAll($status = null) {
$query = "SELECT * FROM " . $this->table_name;
if ($status) {
$query .= " WHERE status = :status";
}
$query .= " ORDER BY building ASC, floor ASC";
$stmt = $this->conn->prepare($query);
if ($status) {
$stmt->bindParam(':status', $status);
}
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
public function getById($id) {
$query = "SELECT * FROM " . $this->table_name . " WHERE id = :id LIMIT 1";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':id', $id);
$stmt->execute();
if ($stmt->rowCount() > 0) {
return $stmt->fetch(PDO::FETCH_ASSOC);
}
return false;
}
public function create($data) {
$query = "INSERT INTO " . $this->table_name . "
(name, building, floor, image_path, transport_center_location, status)
VALUES (:name, :building, :floor, :image_path, :transport_center_location, :status)";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':name', $data['name']);
$stmt->bindParam(':building', $data['building']);
$stmt->bindParam(':floor', $data['floor']);
$stmt->bindParam(':image_path', $data['image_path']);
$stmt->bindParam(':transport_center_location', $data['transport_center_location']);
$stmt->bindParam(':status', $data['status']);
return $stmt->execute();
}
public function update($id, $data) {
$update_fields = [];
$params = [':id' => $id];
foreach (['name', 'building', 'floor', 'transport_center_location', 'status'] as $field) {
if (isset($data[$field])) {
$update_fields[] = "{$field} = :{$field}";
$params[":{$field}"] = $data[$field];
}
}
// Update image only if provided
if (isset($data['image_path'])) {
$update_fields[] = "image_path = :image_path";
$params[":image_path"] = $data['image_path'];
}
if (empty($update_fields)) return false;
$query = "UPDATE " . $this->table_name . " SET " . implode(", ", $update_fields) . " WHERE id = :id";
$stmt = $this->conn->prepare($query);
foreach ($params as $key => &$val) {
$stmt->bindParam($key, $val);
}
return $stmt->execute();
}
public function delete($id) {
// Find image path to delete file
$map = $this->getById($id);
if ($map && !empty($map['image_path'])) {
$file_path = dirname(__DIR__) . '/' . $map['image_path'];
if (file_exists($file_path)) {
unlink($file_path);
}
}
$query = "DELETE FROM " . $this->table_name . " WHERE id = :id";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':id', $id);
return $stmt->execute();
}
}
@@ -0,0 +1,312 @@
<?php
// models/Job.php
require_once dirname(__DIR__) . '/config/database.php';
require_once __DIR__ . '/Department.php';
require_once __DIR__ . '/Notification.php';
require_once __DIR__ . '/User.php';
require_once __DIR__ . '/DispatchEngine.php';
class Job {
private $conn;
private $table_name = "jobs";
public function __construct() {
$this->conn = Database::getInstance();
}
public function create($data) {
// Generate unique job number
$job_number = "JOB-" . date('Ymd') . "-" . str_pad(rand(1, 999), 3, '0', STR_PAD_LEFT);
$query = "INSERT INTO " . $this->table_name . "
(job_number, patient_hn, patient_name, patient_image, from_department_id, to_department_id, to_department_custom, equipment_type, priority, requester_id, notes, is_scheduled, scheduled_time, infection_control, is_round_trip)
VALUES (:job_number, :patient_hn, :patient_name, :patient_image, :from_department_id, :to_department_id, :to_department_custom, :equipment_type, :priority, :requester_id, :notes, :is_scheduled, :scheduled_time, :infection_control, :is_round_trip)";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':job_number', $job_number);
$stmt->bindParam(':patient_hn', $data['patient_hn']);
$stmt->bindValue(':patient_name', $data['patient_name'], PDO::PARAM_STR);
$stmt->bindValue(':patient_image', $data['patient_image'] ?? null, PDO::PARAM_STR);
$stmt->bindParam(':from_department_id', $data['from_department_id']);
$stmt->bindValue(':to_department_id', $data['to_department_id'], PDO::PARAM_INT);
$stmt->bindValue(':to_department_custom', $data['to_department_custom'] ?? null, PDO::PARAM_STR);
$stmt->bindParam(':equipment_type', $data['equipment_type']);
$stmt->bindParam(':priority', $data['priority']);
$stmt->bindParam(':requester_id', $data['requester_id']);
$stmt->bindParam(':notes', $data['notes']);
$is_scheduled = isset($data['is_scheduled']) ? (int)$data['is_scheduled'] : 0;
$scheduled_time = !empty($data['scheduled_time']) ? $data['scheduled_time'] : null;
$infection_control = isset($data['infection_control']) ? (int)$data['infection_control'] : 0;
$is_round_trip = isset($data['is_round_trip']) ? (int)$data['is_round_trip'] : 0;
$stmt->bindParam(':is_scheduled', $is_scheduled);
$stmt->bindValue(':scheduled_time', $scheduled_time, PDO::PARAM_STR);
$stmt->bindParam(':infection_control', $infection_control);
$stmt->bindParam(':is_round_trip', $is_round_trip);
if ($stmt->execute()) {
$job_id = $this->conn->lastInsertId();
$this->logTimeline($job_id, 'pending', 'สร้างคำของานเปล', $data['requester_id']);
// Send Notification
try {
$deptModel = new Department();
$from_dept = $deptModel->getById($data['from_department_id']);
$to_dept = $deptModel->getById($data['to_department_id']);
$from_name = $from_dept ? $from_dept['name'] : 'ไม่ระบุ';
$to_name = $to_dept ? $to_dept['name'] : 'ไม่ระบุ';
$notification = new Notification();
$msg = "🚨 มีคำของานเปลใหม่ 🚨\n";
$msg .= "เลขที่: " . $job_number . "\n";
$msg .= "ผู้ป่วย: " . $data['patient_name'] . " (HN: " . $data['patient_hn'] . ")\n";
$msg .= "จุดรับ: " . $from_name . "\n";
$msg .= "จุดส่ง: " . $to_name . "\n";
$msg .= "ความฉุกเฉิน: " . $data['priority'] . "\n";
$msg .= "อุปกรณ์: " . $data['equipment_type'];
if ($infection_control == 1) {
$msg .= "\n⚠️ ผู้ป่วยแยกโรค / เสี่ยงติดเชื้อ (Infection Control)";
}
if ($is_scheduled == 1) {
$msg .= "\n⏰ เวลานัดหมาย: " . $scheduled_time;
}
if (!empty($data['notes'])) {
$msg .= "\nหมายเหตุ: " . $data['notes'];
}
$notification->sendToAll($msg);
} catch (Exception $e) {
// Silently fail if notification fails, don't break job creation
}
// Run Auto Dispatch Engine only if it's not a scheduled job in the future
if ($is_scheduled == 0) {
try {
$dispatchEngine = new DispatchEngine();
$dispatchEngine->runAutoDispatch($job_id);
} catch (Exception $e) {
// Fail silently
}
}
return $job_number;
}
return false;
}
public function getById($id) {
$query = "SELECT * FROM " . $this->table_name . " WHERE id = :id LIMIT 1";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':id', $id);
$stmt->execute();
if ($stmt->rowCount() > 0) {
return $stmt->fetch(PDO::FETCH_ASSOC);
}
return false;
}
public function getByJobNumber($job_number) {
$query = "SELECT * FROM " . $this->table_name . " WHERE job_number = :job_number LIMIT 1";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':job_number', $job_number);
$stmt->execute();
if ($stmt->rowCount() > 0) {
return $stmt->fetch();
}
return false;
}
public function logTimeline($job_id, $status, $description, $user_id) {
$query = "INSERT INTO job_timelines (job_id, status, description, created_by) VALUES (:job_id, :status, :description, :created_by)";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':job_id', $job_id);
$stmt->bindParam(':status', $status);
$stmt->bindParam(':description', $description);
$stmt->bindParam(':created_by', $user_id);
$stmt->execute();
}
public function getActiveJobByStaff($staff_id) {
$query = "SELECT j.*,
fd.name as from_department_name,
td.name as to_department_name
FROM " . $this->table_name . " j
LEFT JOIN departments fd ON j.from_department_id = fd.id
LEFT JOIN departments td ON j.to_department_id = td.id
WHERE j.assigned_to = :staff_id
AND j.status IN ('assigned', 'accepted', 'in_progress', 'waiting_for_return')
ORDER BY j.id DESC LIMIT 1";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':staff_id', $staff_id);
$stmt->execute();
if ($stmt->rowCount() > 0) {
return $stmt->fetch(PDO::FETCH_ASSOC);
}
return false;
}
public function updateStatus($job_id, $status, $user_id) {
$update_field = "";
if ($status === 'accepted') {
$update_field = ", accepted_at = NOW()";
} elseif ($status === 'in_progress') {
$update_field = ", pickup_at = NOW()";
} elseif ($status === 'completed') {
$update_field = ", completed_at = NOW()";
}
$query = "UPDATE " . $this->table_name . "
SET status = :status {$update_field}
WHERE id = :job_id";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':status', $status);
$stmt->bindParam(':job_id', $job_id);
if ($stmt->execute()) {
$description = "อัปเดตสถานะเป็น: " . $status;
$this->logTimeline($job_id, $status, $description, $user_id);
// Send Notification for key events
try {
if (in_array($status, ['accepted', 'completed'])) {
$job = $this->getById($job_id);
$job_number = $job ? $job['job_number'] : $job_id;
$userModel = new User();
$user = $userModel->getById($user_id);
$user_name = $user ? $user['full_name'] : 'ระบบ';
$notification = new Notification();
if ($status === 'accepted') {
$msg = "✅ เจ้าหน้าที่รับงานแล้ว\nงาน: " . $job_number . "\nผู้รับงาน: " . $user_name;
} else if ($status === 'completed') {
$msg = "🏁 งานเสร็จสิ้น\nงาน: " . $job_number . "\nผู้ดำเนินการ: " . $user_name;
}
$notification->sendToAll($msg);
}
} catch (Exception $e) {
// Fail silently
}
return true;
}
return false;
}
public function cancelJob($job_id, $reason, $user_id) {
$status = 'cancelled';
// Append reason to notes
$query = "UPDATE " . $this->table_name . "
SET status = :status,
notes = CONCAT(IFNULL(notes, ''), '\n[ยกเลิก: ', :reason, ']')
WHERE id = :job_id";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':status', $status);
$stmt->bindParam(':reason', $reason);
$stmt->bindParam(':job_id', $job_id);
if ($stmt->execute()) {
$description = "ยกเลิกงาน เหตุผล: " . $reason;
$this->logTimeline($job_id, $status, $description, $user_id);
// Send Notification for cancellation
try {
$job = $this->getById($job_id);
$job_number = $job ? $job['job_number'] : $job_id;
$userModel = new User();
$user = $userModel->getById($user_id);
$user_name = $user ? $user['full_name'] : 'ระบบ';
$notification = new Notification();
$msg = "❌ ปฏิเสธ/ยกเลิกงาน\nงาน: " . $job_number . "\nเหตุผล: " . $reason . "\nผู้ยกเลิก: " . $user_name;
$notification->sendToAll($msg);
} catch (Exception $e) {
// Fail silently
}
return true;
}
return false;
}
public function getJobTimelines($job_id) {
$query = "SELECT t.*, u.full_name as created_by_name
FROM job_timelines t
LEFT JOIN users u ON t.created_by = u.id
WHERE t.job_id = :job_id
ORDER BY t.created_at ASC";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':job_id', $job_id);
$stmt->execute();
return $stmt->fetchAll();
}
public function getStaffHistory($staff_id, $limit = 50) {
$query = "SELECT j.*,
fd.name as from_department_name,
td.name as to_department_name
FROM " . $this->table_name . " j
LEFT JOIN departments fd ON j.from_department_id = fd.id
LEFT JOIN departments td ON j.to_department_id = td.id
WHERE j.assigned_to = :staff_id AND j.status IN ('completed', 'cancelled')
ORDER BY j.completed_at DESC, j.created_at DESC LIMIT :limit";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':staff_id', $staff_id);
$stmt->bindValue(':limit', (int)$limit, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll();
}
public function getStaffPerformance($date_range = 'today') {
$date_condition = "DATE(j.created_at) = CURDATE()";
if ($date_range === 'week') {
$date_condition = "YEARWEEK(j.created_at, 1) = YEARWEEK(CURDATE(), 1)";
} elseif ($date_range === 'month') {
$date_condition = "YEAR(j.created_at) = YEAR(CURDATE()) AND MONTH(j.created_at) = MONTH(CURDATE())";
}
$query = "SELECT u.id, u.full_name,
COUNT(j.id) as total_jobs,
SUM(CASE WHEN j.status = 'completed' THEN 1 ELSE 0 END) as completed_jobs,
SUM(CASE WHEN j.status = 'cancelled' THEN 1 ELSE 0 END) as cancelled_jobs,
AVG(TIMESTAMPDIFF(MINUTE, j.accepted_at, j.completed_at)) as avg_service_time
FROM users u
LEFT JOIN " . $this->table_name . " j ON u.id = j.assigned_to AND " . $date_condition . "
WHERE u.role = 'staff'
GROUP BY u.id, u.full_name
ORDER BY completed_jobs DESC";
$stmt = $this->conn->prepare($query);
$stmt->execute();
return $stmt->fetchAll();
}
public function getStaffStats($staff_id, $date_range = 'today') {
$date_condition = "DATE(created_at) = CURDATE()";
if ($date_range === 'week') {
$date_condition = "YEARWEEK(created_at, 1) = YEARWEEK(CURDATE(), 1)";
} elseif ($date_range === 'month') {
$date_condition = "YEAR(created_at) = YEAR(CURDATE()) AND MONTH(created_at) = MONTH(CURDATE())";
}
$query = "SELECT
COUNT(id) as total_jobs,
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed_jobs,
SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) as cancelled_jobs,
AVG(TIMESTAMPDIFF(MINUTE, accepted_at, completed_at)) as avg_service_time
FROM " . $this->table_name . "
WHERE assigned_to = :staff_id AND " . $date_condition;
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':staff_id', $staff_id);
$stmt->execute();
return $stmt->fetch();
}
}
@@ -0,0 +1,78 @@
<?php
// models/Notification.php
require_once __DIR__ . '/Setting.php';
class Notification {
private $settingModel;
private $lineToken;
private $telegramToken;
private $telegramChatId;
public function __construct() {
$this->settingModel = new Setting();
$settings = $this->settingModel->getAllFlat();
$this->lineToken = $settings['line_notify_token'] ?? null;
$this->telegramToken = $settings['telegram_bot_token'] ?? null;
$this->telegramChatId = $settings['telegram_chat_id'] ?? null;
}
/**
* Send a simple text message to all configured platforms
*/
public function sendToAll($message) {
if (!empty($this->lineToken)) {
$this->sendToLine($message);
}
if (!empty($this->telegramToken) && !empty($this->telegramChatId)) {
$this->sendToTelegram($message);
}
}
/**
* Send message via LINE Notify
*/
private function sendToLine($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(['message' => "\n" . $message]));
$headers = [
'Content-type: application/x-www-form-urlencoded',
'Authorization: Bearer ' . $this->lineToken,
];
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result = curl_exec($ch);
curl_close($ch);
return $result;
}
/**
* Send message via Telegram Bot
*/
private function sendToTelegram($message) {
$url = "https://api.telegram.org/bot" . $this->telegramToken . "/sendMessage";
$data = [
'chat_id' => $this->telegramChatId,
'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);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$result = curl_exec($ch);
curl_close($ch);
return $result;
}
}
@@ -0,0 +1,23 @@
<?php
// models/Patient.php
// Mock class to simulate HIS Integration
class Patient {
// In a real application, this would connect to the HIS Database (e.g., HOSxP)
// using a separate PDO connection in Read-Only mode.
public static function searchByHN($hn) {
// Mock data
$mock_patients = [
'123456' => ['hn' => '123456', 'name' => 'นายใจดี รักสงบ', 'age' => 45, 'gender' => 'ชาย'],
'654321' => ['hn' => '654321', 'name' => 'ด.ญ.มานี มีตา', 'age' => 8, 'gender' => 'หญิง'],
'987654' => ['hn' => '987654', 'name' => 'นายสมหมาย สบายใจ', 'age' => 60, 'gender' => 'ชาย']
];
if (isset($mock_patients[$hn])) {
return $mock_patients[$hn];
}
return null;
}
}
@@ -0,0 +1,157 @@
<?php
// models/Setting.php
require_once dirname(__DIR__) . '/config/database.php';
class Setting {
private $conn;
private $table_name = "settings";
public function __construct() {
$this->conn = Database::getInstance();
}
/**
* ดึงค่า setting ตัวเดียว โดยใช้ key
*/
public function get($key, $default = null) {
$query = "SELECT setting_value FROM " . $this->table_name . " WHERE setting_key = :key LIMIT 1";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':key', $key);
$stmt->execute();
if ($stmt->rowCount() > 0) {
$row = $stmt->fetch();
return $row['setting_value'];
}
return $default;
}
/**
* ตั้งค่า setting ตัวเดียว โดยใช้ key
*/
public function set($key, $value) {
// เช็คว่ามี key นี้อยู่หรือไม่
$check_query = "SELECT id FROM " . $this->table_name . " WHERE setting_key = :key LIMIT 1";
$check_stmt = $this->conn->prepare($check_query);
$check_stmt->bindParam(':key', $key);
$check_stmt->execute();
if ($check_stmt->rowCount() > 0) {
$query = "UPDATE " . $this->table_name . " SET setting_value = :value WHERE setting_key = :key";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':key', $key);
$stmt->bindParam(':value', $value);
} else {
// สมมติ category เป็น 'general' และ label ให้เท่ากับ key สำหรับค่าใหม่ (ถ้ามี)
$query = "INSERT INTO " . $this->table_name . " (setting_key, setting_value, category, label) VALUES (:key, :value, 'general', :label)";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':key', $key);
$stmt->bindParam(':value', $value);
$stmt->bindParam(':label', $key);
}
return $stmt->execute();
}
/**
* ดึงค่า setting ทั้งหมด โดยแบ่งตาม category
*/
public function getAllByCategory() {
$query = "SELECT * FROM " . $this->table_name . " ORDER BY category, id";
$stmt = $this->conn->prepare($query);
$stmt->execute();
$results = [];
while ($row = $stmt->fetch()) {
$cat = $row['category'];
if (!isset($results[$cat])) {
$results[$cat] = [];
}
$results[$cat][] = $row;
}
return $results;
}
/**
* ดึงค่า setting ทั้งหมดแบบ Flat Array (key => value)
*/
public function getAllFlat() {
$query = "SELECT setting_key, setting_value FROM " . $this->table_name;
$stmt = $this->conn->prepare($query);
$stmt->execute();
$results = [];
while ($row = $stmt->fetch()) {
$results[$row['setting_key']] = $row['setting_value'];
}
return $results;
}
/**
* ดึงค่า setting ทั้งหมด โดยจัดกลุ่มเป็น associative array: category => [key => value]
*/
public function getAllGrouped() {
$query = "SELECT * FROM " . $this->table_name;
$stmt = $this->conn->prepare($query);
$stmt->execute();
$results = [];
while ($row = $stmt->fetch()) {
$cat = $row['category'];
$key = $row['setting_key'];
$val = $row['setting_value'];
if (!isset($results[$cat])) {
$results[$cat] = [];
}
$results[$cat][$key] = $val;
}
return $results;
}
/**
* ดึงค่า setting เฉพาะกลุ่ม (Category) แบบ Associative Array
*/
public function getGroup($category) {
$query = "SELECT setting_key, setting_value FROM " . $this->table_name . " WHERE category = :cat";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':cat', $category);
$stmt->execute();
$results = [];
while ($row = $stmt->fetch()) {
$results[$row['setting_key']] = $row['setting_value'];
}
// If empty, let's also try to fetch from flat array just in case it was inserted into 'general'
// This makes it backwards compatible with newly inserted settings.
if (empty($results)) {
$flat = $this->getAllFlat();
return $flat;
}
return $results;
}
/**
* อัปเดตค่า setting หลายตัวพร้อมกัน
*/
public function updateBulk($settings_array) {
$query = "UPDATE " . $this->table_name . " SET setting_value = :val WHERE setting_key = :key";
$stmt = $this->conn->prepare($query);
$this->conn->beginTransaction();
try {
foreach ($settings_array as $key => $val) {
$stmt->bindValue(':val', $val);
$stmt->bindValue(':key', $key);
$stmt->execute();
}
$this->conn->commit();
return true;
} catch (Exception $e) {
$this->conn->rollBack();
return false;
}
}
}
@@ -0,0 +1,169 @@
<?php
// models/User.php
require_once dirname(__DIR__) . '/config/database.php';
class User {
private $conn;
private $table_name = "users";
public $id;
public $username;
public $password;
public $full_name;
public $role;
public $department_id;
public $status;
public function __construct() {
$this->conn = Database::getInstance();
}
public function login($username, $password) {
$query = "SELECT id, username, password, full_name, role, department_id
FROM " . $this->table_name . "
WHERE username = :username LIMIT 0,1";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':username', $username);
$stmt->execute();
if ($stmt->rowCount() > 0) {
$row = $stmt->fetch();
if (password_verify($password, $row['password'])) {
// Set object properties
$this->id = $row['id'];
$this->username = $row['username'];
$this->full_name = $row['full_name'];
$this->role = $row['role'];
$this->department_id = $row['department_id'];
// Update status to online
$this->updateStatus($this->id, 'online');
return true;
}
}
return false;
}
public function updateStatus($user_id, $status) {
$query = "UPDATE " . $this->table_name . " SET status = :status WHERE id = :id";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':status', $status);
$stmt->bindParam(':id', $user_id);
$stmt->execute();
}
public function getById($id) {
$query = "SELECT * FROM " . $this->table_name . " WHERE id = :id LIMIT 1";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':id', $id);
$stmt->execute();
if ($stmt->rowCount() > 0) {
return $stmt->fetch();
}
return false;
}
public function getAll() {
$query = "SELECT u.*, d.name as department_name
FROM " . $this->table_name . " u
LEFT JOIN departments d ON u.department_id = d.id
ORDER BY u.created_at DESC";
$stmt = $this->conn->prepare($query);
$stmt->execute();
return $stmt->fetchAll();
}
public function add($username, $password, $full_name, $role, $department_id = null) {
$query = "INSERT INTO " . $this->table_name . " (username, password, full_name, role, department_id, status)
VALUES (:username, :password, :full_name, :role, :department_id, 'offline')";
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':username', $username);
$stmt->bindParam(':password', $hashed_password);
$stmt->bindParam(':full_name', $full_name);
$stmt->bindParam(':role', $role);
if ($department_id) {
$stmt->bindParam(':department_id', $department_id);
} else {
$stmt->bindValue(':department_id', null, PDO::PARAM_NULL);
}
return $stmt->execute();
}
public function update($id, $full_name, $role, $department_id = null) {
$query = "UPDATE " . $this->table_name . "
SET full_name = :full_name, role = :role, department_id = :department_id
WHERE id = :id";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':full_name', $full_name);
$stmt->bindParam(':role', $role);
$stmt->bindParam(':id', $id);
if ($department_id) {
$stmt->bindParam(':department_id', $department_id);
} else {
$stmt->bindValue(':department_id', null, PDO::PARAM_NULL);
}
return $stmt->execute();
}
public function updatePassword($id, $password) {
$query = "UPDATE " . $this->table_name . " SET password = :password WHERE id = :id";
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':password', $hashed_password);
$stmt->bindParam(':id', $id);
return $stmt->execute();
}
public function delete($id) {
// Since we have foreign keys, we might want to just set status or delete.
// Assuming delete for now based on schema constraints (SET NULL or CASCADE might be needed)
// Schema has jobs.requester_id and jobs.assigned_to referencing users(id).
// Deleting a user with jobs will fail unless we handle it, or we can soft delete.
// For simplicity, let's hard delete, but if it fails it throws PDOException.
$query = "DELETE FROM " . $this->table_name . " WHERE id = :id";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':id', $id);
return $stmt->execute();
}
public function getAvailableStaff() {
// Find staff who are online and NOT currently handling an active job
// Order by who has been idle the longest today
$query = "
SELECT u.id, u.full_name, MAX(j.completed_at) as last_job
FROM " . $this->table_name . " u
LEFT JOIN jobs j ON u.id = j.assigned_to
AND j.status = 'completed'
AND DATE(j.created_at) = CURDATE()
WHERE u.role = 'staff'
AND u.status = 'online'
AND u.id NOT IN (
SELECT assigned_to
FROM jobs
WHERE status IN ('assigned', 'accepted', 'in_progress')
AND assigned_to IS NOT NULL
)
GROUP BY u.id
ORDER BY last_job ASC
LIMIT 1
";
$stmt = $this->conn->query($query);
if ($stmt->rowCount() > 0) {
return $stmt->fetch(PDO::FETCH_ASSOC);
}
return false;
}
}