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(); } }