313 lines
14 KiB
PHP
313 lines
14 KiB
PHP
<?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();
|
|
}
|
|
}
|