LeaveApplicationHandler.php

24.10 KB
08/07/2025 10:47
PHP
LeaveApplicationHandler.php
<?php

namespace App\Handlers;

use App\Core\Database;
use App\Models\Employee;
use App\Models\LeaveRequest;
use App\Models\LeaveType;
use App\Services\TelegramService;
use DateTime;

class LeaveApplicationHandler
{
    private TelegramService $telegramService;
    private int $chatId;
    private int $userId; // Telegram User ID
    private ?int $employeeId = null; // System's employee ID
    private string $requestMode; // 'leave' or 'wfh' or specific alias

    /**
     * @param TelegramService $telegramService
     * @param int $chatId
     * @param int $userId
     * @param string $requestMode
     */
    public function __construct(TelegramService $telegramService, int $chatId, int $userId, string $requestMode = 'leave')
    {
        $this->telegramService = $telegramService;
        $this->chatId = $chatId;
        $this->userId = $userId;
        $this->requestMode = $requestMode; // e.g., 'leave', 'wfh'
        $this->loadEmployeeId();
    }

    private function loadEmployeeId(): void
    {
        $employeeModel = new Employee(Database::getInstance());
        $employee = $employeeModel->findByChatUserId((string) $this->userId);
        if ($employee && $employee->id) {
            $this->employeeId = $employee->id;
            log_message("Employee ID {$this->employeeId} loaded for Telegram user ID {$this->userId} in mode {$this->requestMode}");
        } else {
            log_message("No employee record found for Telegram user ID {$this->userId}.");
        }
    }

    private function clearUserState(): void
    {
        $stateFile = __DIR__."/../../logs/user_{$this->userId}_chat_{$this->chatId}_state.txt";
        if (file_exists($stateFile)) {
            if (unlink($stateFile)) {
                log_message("State cleared by Handler for user {$this->userId} chat {$this->chatId}: {$stateFile}");
            } else {
                log_message("Failed to clear state file by Handler for user {$this->userId} chat {$this->chatId}: {$stateFile}");
            }
        }
    }

    /**
     * @param string $state
     */
    private function saveUserState(string $state): void
    {
        // Prefix state with mode to avoid collision if user switches mode mid-conversation (though /cancel should prevent this)
        $statePrefix = $this->requestMode === 'wfh' ? 'wfh_' : 'la_';
        $fullState = $statePrefix.$state; // e.g. la_expect_startDate_for_1 or wfh_expect_startDate_for_5
        $stateFilePath = __DIR__."/../../logs/user_{$this->userId}_chat_{$this->chatId}_state.txt";
        if (file_put_contents($stateFilePath, $fullState) === false) {
            log_message("Failed to save state by Handler to {$stateFilePath}: {$fullState}");
        } else {
            log_message("State saved by Handler to {$stateFilePath}: {$fullState}");
        }
    }

    /**
     * Starts the leave/wfh request process.
     * If an alias is provided (e.g., 'wfh'), it tries to find a unique leave type for it.
     * If not, or if mode is 'leave', it shows all applicable leave types.
     */
    public function initiateRequest(): void
    {
        if ($this->employeeId === null) {
            $this->telegramService->sendMessage($this->chatId, "ขออภัยค่ะ ไม่พบข้อมูลพนักงานของคุณ (Telegram User ID: {$this->userId}) ในระบบ โปรดติดต่อ HR เพื่อลงทะเบียน Telegram User ID ของคุณก่อนเริ่มใช้งานค่ะ");
            $this->clearUserState();
            return;
        }
        $this->clearUserState(); // Clear previous state before starting a new request

        $leaveTypeModel = new LeaveType(Database::getInstance());

        if ($this->requestMode === 'wfh') {
            $wfhType = $leaveTypeModel->findByAlias('wfh');
            if ($wfhType && $wfhType->id) {
                log_message("Unique WFH type found (ID: {$wfhType->id}) for user {$this->userId}. Asking for start date.");
                $this->saveUserState("expect_startDate_for_{$wfhType->id}");
                $this->telegramService->sendMessage($this->chatId, "คุณกำลังขอ Work From Home (ประเภท: {$wfhType->name}).\nกรุณาพิมพ์วันที่เริ่ม (รูปแบบ YYYY-MM-DD):");
                return;
            } else {
                log_message("No unique WFH type found for alias 'wfh'. Will show list of types or specific WFH types if implemented.");
                // Fallback to showing list. You might want to filter for WFH-specific types here if you have many.
                // For now, it will show all types if the specific 'wfh' alias is not found or unique.
            }
        }

        $leaveTypesData = $leaveTypeModel->read();

        if (empty($leaveTypesData)) {
            $this->telegramService->sendMessage($this->chatId, "ขออภัยค่ะ ตอนนี้ยังไม่มีประเภทการลา/WFH ในระบบเลยค่ะ");
            return;
        }

        $keyboardButtons = [];
        foreach ($leaveTypesData as $type) {
            if (is_array($type) && isset($type['id']) && isset($type['name'])) {
                $keyboardButtons[] = [['text' => $type['name'], 'callback_data' => "req_type_{$type['id']}"]];
            }
        }

        if (empty($keyboardButtons)) {
            $this->telegramService->sendMessage($this->chatId, "มีประเภทการลา/WFH ในระบบ แต่ไม่สามารถสร้างปุ่มได้ค่ะ กรุณาติดต่อผู้ดูแล");
            return;
        }
        $replyMarkup = ['inline_keyboard' => $keyboardButtons];
        $promptMessage = $this->requestMode === 'wfh' ? "ไม่พบประเภท WFH อัตโนมัติ, กรุณาเลือกประเภท Work From Home ที่ต้องการค่ะ:" : "สวัสดีค่ะ (พนักงาน ID: {$this->employeeId}) ต้องการลาประเภทไหนคะ?";
        $this->telegramService->sendMessage($this->chatId, $promptMessage, $replyMarkup);
    }

    /**
     * @param string $callbackData
     * @return null
     */
    public function handleTypeSelection(string $callbackData): void
    {
        if ($this->employeeId === null) {
            $this->telegramService->sendMessage($this->chatId, "ขออภัยค่ะ ไม่พบข้อมูลพนักงานของคุณ");
            $this->clearUserState();return;
        }

        $parts = explode('_', $callbackData); // req_type_{id}
        $leaveTypeId = (int) end($parts);

        $leaveTypeModel = new LeaveType(Database::getInstance());
        $selectedTypeObj = $leaveTypeModel->read($leaveTypeId);

        if ($selectedTypeObj && $selectedTypeObj->id) {
            $this->saveUserState("expect_startDate_for_{$leaveTypeId}");
            $prompt = ($this->requestMode === 'wfh' ? "ขอ WFH" : "ลา")."ประเภท: {$selectedTypeObj->name}.\nกรุณาพิมพ์วันที่เริ่ม (รูปแบบ YYYY-MM-DD):";
            $this->telegramService->sendMessage($this->chatId, $prompt);
        } else {
            $this->telegramService->sendMessage($this->chatId, "ขออภัยค่ะ ไม่พบประเภทที่คุณเลือก (ID: {$leaveTypeId})");
            $this->clearUserState();
        }
    }

    /**
     * @param string $stateData
     * @param string $dateString
     * @return null
     */
    public function handleStartDateInput(string $stateData, string $dateString): void
    {
        if ($this->employeeId === null) {
            $this->telegramService->sendMessage($this->chatId, "ขออภัยค่ะ ไม่พบข้อมูลพนักงานของคุณ");
            $this->clearUserState();return;
        }

        $stateWithoutPrefix = preg_replace('/^(la_|wfh_)/', '', $stateData);
        $parts = explode('_', $stateWithoutPrefix); // expect_startDate_for_{id}
        $leaveTypeId = (int) end($parts);

        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateString) || !($startDate = DateTime::createFromFormat('Y-m-d', $dateString)) || $startDate->format('Y-m-d') !== $dateString) {
            $this->telegramService->sendMessage($this->chatId, "รูปแบบวันที่เริ่มไม่ถูกต้อง ({$dateString}) กรุณาใช้ YYYY-MM-DD. ลองอีกครั้งค่ะ:");
            return;
        }

        $today = new DateTime();
        $today->setTime(0, 0, 0);
        $sevenDaysAgo = (new DateTime())->modify('-7 days')->setTime(0, 0, 0);
        if ($startDate < $sevenDaysAgo) {
            $this->telegramService->sendMessage($this->chatId, "วันที่เริ่ม ({$dateString}) ไม่ควรเป็นอดีตนานเกินไปค่ะ");
            return;
        }

        $this->saveUserState("expect_endDate_for_{$leaveTypeId}_start_{$dateString}");
        $this->telegramService->sendMessage($this->chatId, "วันที่เริ่ม: {$dateString}.\nกรุณาพิมพ์วันที่สิ้นสุด (รูปแบบ YYYY-MM-DD):");
    }

    /**
     * @param string $stateData
     * @param string $endDateString
     * @return null
     */
    public function handleEndDateInput(string $stateData, string $endDateString): void
    {
        if ($this->employeeId === null) {
            $this->telegramService->sendMessage($this->chatId, "ขออภัยค่ะ ไม่พบข้อมูลพนักงานของคุณ");
            $this->clearUserState();return;
        }
        $stateWithoutPrefix = preg_replace('/^(la_|wfh_)/', '', $stateData);
        preg_match('/expect_endDate_for_(\d+)_start_(\d{4}-\d{2}-\d{2})/', $stateWithoutPrefix, $matches);
        if (count($matches) !== 3) {
            $this->telegramService->sendMessage($this->chatId, "เกิดข้อผิดพลาดในการอ่าน state (end date) ค่ะ ลอง /cancel แล้วเริ่มใหม่นะคะ");
            $this->clearUserState();return;
        }
        $leaveTypeId = (int) $matches[1];
        $startDateString = $matches[2];

        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $endDateString) || !($endDate = DateTime::createFromFormat('Y-m-d', $endDateString)) || $endDate->format('Y-m-d') !== $endDateString) {
            $this->telegramService->sendMessage($this->chatId, "รูปแบบวันที่สิ้นสุดไม่ถูกต้อง ({$endDateString}). ลองอีกครั้งค่ะ:");
            return;
        }
        if (strtotime($endDateString) < strtotime($startDateString)) {
            $this->telegramService->sendMessage($this->chatId, "วันที่สิ้นสุด ({$endDateString}) ต้องไม่มาก่อนวันที่เริ่ม ({$startDateString}). ลองอีกครั้งค่ะ:");
            return;
        }

        $this->saveUserState("expect_reason_for_{$leaveTypeId}_start_{$startDateString}_end_{$endDateString}");
        $promptMessage = $this->requestMode === 'wfh' ? "กรุณาใส่หมายเหตุ (ถ้ามี, ถ้าไม่มีพิมพ์ '-'):" : "กรุณาใส่เหตุผลการลา (ถ้าไม่มี พิมพ์ 'ไม่มี' หรือ '-' ):";
        $this->telegramService->sendMessage($this->chatId, "วันที่เริ่ม: {$startDateString}, สิ้นสุด: {$endDateString}.\n{$promptMessage}");
    }

    /**
     * @param string $stateData
     * @param string $reasonText
     * @return null
     */
    public function handleReasonInput(string $stateData, string $reasonText): void
    {
        if ($this->employeeId === null) {
            $this->telegramService->sendMessage($this->chatId, "ขออภัยค่ะ ไม่พบข้อมูลพนักงานของคุณ");
            $this->clearUserState();return;
        }
        $stateWithoutPrefix = preg_replace('/^(la_|wfh_)/', '', $stateData);
        preg_match('/expect_reason_for_(\d+)_start_(\d{4}-\d{2}-\d{2})_end_(\d{4}-\d{2}-\d{2})/', $stateWithoutPrefix, $matches);
        if (count($matches) !== 4) {
            $this->telegramService->sendMessage($this->chatId, "เกิดข้อผิดพลาดในการอ่าน state (reason) ค่ะ ลอง /cancel แล้วเริ่มใหม่นะคะ");
            $this->clearUserState();return;
        }
        $leaveTypeId = (int) $matches[1];
        $startDateString = $matches[2];
        $endDateString = $matches[3];

        $reason = (strtolower(trim($reasonText)) === 'ไม่มี' || trim($reasonText) === '-') ? null : trim($reasonText);

        $leaveTypeModel = new LeaveType(Database::getInstance());
        $selectedType = $leaveTypeModel->read($leaveTypeId);
        $typeName = ($selectedType && $selectedType->id) ? $selectedType->name : "N/A (ID: {$leaveTypeId})";

        $escapeMarkdown = function ($text) {
            if ($text === null) {
                return "ไม่ได้ระบุ";
            }

            // More comprehensive list from Telegram Bot API docs for MarkdownV2
            return preg_replace('/([_*\[\]()~`>#\+\-=|{}.!\\\\])/', '\\\\$1', $text);
        };

        $typeNameSafe = $escapeMarkdown($typeName);
        $startDateSafe = $escapeMarkdown($startDateString);
        $endDateSafe = $escapeMarkdown($endDateString);
        $reasonSafe = $escapeMarkdown($reason);

        $summaryTitle = $this->requestMode === 'wfh' ? "*สรุปคำขอ WFH ของคุณ:*\n" : "*สรุปคำขอลาของคุณ:*\n";
        $summary = $summaryTitle;
        $summary .= "ประเภท: {$typeNameSafe}\n";
        $summary .= "วันที่เริ่ม: {$startDateSafe}\n";
        $summary .= "วันที่สิ้นสุด: {$endDateSafe}\n";
        $summary .= ($this->requestMode === 'wfh' ? "หมายเหตุ: " : "เหตุผล: ").$reasonSafe."\n\n";
        $summary .= "กรุณายืนยันข้อมูลค่ะ";

        $tempRequestData = json_encode([
            'employee_id' => $this->employeeId,
            'leave_type_id' => $leaveTypeId,
            'start_date' => $startDateString,
            'end_date' => $endDateString,
            'reason' => $reason,
            'mode' => $this->requestMode
        ]);

        $this->saveUserState("confirm_".base64_encode($tempRequestData));

        $confirmButtonText = $this->requestMode === 'wfh' ? '✅ ยืนยัน WFH' : '✅ ยืนยันการลา';
        $keyboardButtons = [
            [['text' => $confirmButtonText, 'callback_data' => 'req_action_confirm']],
            [['text' => '❌ ยกเลิก', 'callback_data' => 'req_action_cancel']]
        ];
        $replyMarkup = ['inline_keyboard' => $keyboardButtons];

        $this->telegramService->sendMessage($this->chatId, $summary, $replyMarkup, 'MarkdownV2');
    }

    /**
     * @param string $stateData
     * @param string $actionCallbackData
     * @return null
     */
    public function handleConfirmationAction(string $stateData, string $actionCallbackData): void
    {
        if ($this->employeeId === null) {
            $this->telegramService->sendMessage($this->chatId, "ขออภัยค่ะ ไม่พบข้อมูลพนักงานของคุณ");
            $this->clearUserState();return;
        }

        $stateWithoutPrefix = preg_replace('/^(la_|wfh_)/', '', $stateData);

        if ($actionCallbackData === 'req_action_cancel') {
            $message = $this->requestMode === 'wfh' ? "การขอ WFH ถูกยกเลิกแล้วค่ะ" : "การขอลาถูกยกเลิกแล้วค่ะ";
            $this->telegramService->sendMessage($this->chatId, $message);
            $this->clearUserState();
            return;
        }

        if ($actionCallbackData === 'req_action_confirm') {
            if (!str_starts_with($stateWithoutPrefix, 'confirm_')) {
                $this->telegramService->sendMessage($this->chatId, "เกิดข้อผิดพลาด: ไม่พบข้อมูลสำหรับยืนยัน (state: {$stateData}). ลองเริ่มใหม่ค่ะ /ลา หรือ /wfh");
                $this->clearUserState();return;
            }

            $base64Data = substr($stateWithoutPrefix, strlen('confirm_'));
            $requestDetails = json_decode(base64_decode($base64Data), true);

            if (!$requestDetails || !isset($requestDetails['employee_id'])) {
                $this->telegramService->sendMessage($this->chatId, "เกิดข้อผิดพลาด: ข้อมูลยืนยันไม่ถูกต้อง. ลองเริ่มใหม่ค่ะ /ลา หรือ /wfh");
                $this->clearUserState();return;
            }
            if ($requestDetails['employee_id'] !== $this->employeeId) {
                $this->telegramService->sendMessage($this->chatId, "เกิดข้อผิดพลาด: ข้อมูลยืนยันไม่ตรงกับผู้ใช้ปัจจุบัน");
                $this->clearUserState();return;
            }

            $leaveRequestModel = new LeaveRequest(Database::getInstance());
            $leaveRequestModel->employee_id = $requestDetails['employee_id'];
            $leaveRequestModel->leave_type_id = $requestDetails['leave_type_id'];
            $leaveRequestModel->start_date = $requestDetails['start_date'];
            $leaveRequestModel->end_date = $requestDetails['end_date'];
            $leaveRequestModel->reason = $requestDetails['reason'];
            $leaveRequestModel->status = 'PENDING';

            if ($leaveRequestModel->create()) {
                $modeInDetails = $requestDetails['mode'] ?? 'leave'; // Fallback to 'leave' if mode not in details
                $newlyCreatedRequestId = $leaveRequestModel->id; // Get the ID of the newly created request
                $modeInDetails = $requestDetails['mode'] ?? 'leave';
                $successMessage = $modeInDetails === 'wfh' ? "✅ คำขอ WFH ของคุณ (ID: {$newlyCreatedRequestId}) ถูกบันทึกเรียบร้อยแล้ว และกำลังรอการอนุมัติค่ะ" : "✅ คำขอลาของคุณ (ID: {$newlyCreatedRequestId}) ถูกบันทึกเรียบร้อยแล้ว และกำลังรอการอนุมัติค่ะ";
                $this->telegramService->sendMessage($this->chatId, $successMessage);

                // --- Notify Manager ---
                $this->notifyManager($newlyCreatedRequestId, $requestDetails);
                // --- End Notify Manager ---

            } else {
                $modeInDetails = $requestDetails['mode'] ?? 'leave';
                $failMessage = $modeInDetails === 'wfh' ? "❌ เกิดข้อผิดพลาดในการบันทึกคำขอ WFH" : "❌ เกิดข้อผิดพลาดในการบันทึกคำขอลา";
                $this->telegramService->sendMessage($this->chatId, "{$failMessage} กรุณาลองใหม่อีกครั้ง หรือติดต่อผู้ดูแลระบบค่ะ");
                error_log("Failed to create request for employee {$this->employeeId}. Mode: {$modeInDetails}. Data: ".json_encode($requestDetails));
            }
            $this->clearUserState();
        }
    }

    /**
     * @param int $leaveRequestId
     * @param array $requestDetails
     * @return null
     */
    private function notifyManager(int $leaveRequestId, array $requestDetails): void
    {
        $employeeModel = new Employee(Database::getInstance());
        // It's better to use $this->employeeId which is already loaded for the requester
        // $requester = $employeeModel->read($requestDetails['employee_id']);
        $requester = $employeeModel->read($this->employeeId);

        if (!$requester || !isset($requester->manager_id) || $requester->manager_id === null) {
            log_message("Cannot notify manager: Requester (ID: {$this->employeeId}) not found in DB or has no manager_id.");
            return;
        }

        $manager = $employeeModel->read($requester->manager_id);
        if (!$manager || !isset($manager->chat_user_id) || $manager->chat_user_id === null) {
            log_message("Cannot notify manager: Manager (ID: {$requester->manager_id}) not found or has no chat_user_id. Requester: {$this->employeeId}");
            // Optionally, notify HR or admin that manager notification failed.
            // $this->telegramService->sendMessage($this->chatId, "(ระบบ: ไม่สามารถแจ้งเตือนหัวหน้าได้เนื่องจากข้อมูลหัวหน้าไม่ครบถ้วน)");
            return;
        }

        // Ensure chat_user_id is treated as integer for Telegram API
        $managerChatId = (int) $manager->chat_user_id;
        if ($managerChatId === 0) {
            // Or any other invalid ID indication
            log_message("Manager (ID: {$manager->id}) has an invalid chat_user_id: {$manager->chat_user_id}. Cannot send notification.");
            return;
        }

        $leaveTypeModel = new LeaveType(Database::getInstance());
        $leaveType = $leaveTypeModel->read($requestDetails['leave_type_id']);
        $leaveTypeName = ($leaveType && isset($leaveType->name)) ? $leaveType->name : 'N/A';

        $requesterName = htmlspecialchars(($requester->first_name ?? 'พนักงาน')." ".($requester->last_name ?? ''));
        $startDateFormatted = (new DateTime($requestDetails['start_date']))->format('d/m/Y');
        $endDateFormatted = (new DateTime($requestDetails['end_date']))->format('d/m/Y');

        $reasonDisplay = "ไม่ได้ระบุ";
        if (!empty($requestDetails['reason'])) {
            // Basic sanitization for message, not for MarkdownV2 here unless specified in sendMessage
            $reasonDisplay = htmlspecialchars($requestDetails['reason']);
        }

        $requestModeText = ($requestDetails['mode'] ?? 'leave') === 'wfh' ? "ขอ Work From Home" : "ขอลา";

        $messageToManager = "เรียน คุณ ".htmlspecialchars($manager->first_name ?? 'หัวหน้า').",\n\n";
        $messageToManager .= "คุณ {$requesterName} (ID: {$this->employeeId}) ได้ส่ง{$requestModeText}เข้ามาใหม่:\n";
        $messageToManager .= "ประเภท: ".htmlspecialchars($leaveTypeName)."\n";
        $messageToManager .= "วันที่: {$startDateFormatted} - {$endDateFormatted}\n";
        $messageToManager .= (($requestDetails['mode'] ?? 'leave') === 'wfh' ? "หมายเหตุ: " : "เหตุผล: ").$reasonDisplay."\n";
        $messageToManager .= "รหัสคำขอ: {$leaveRequestId}\n\n";
        $messageToManager .= "กรุณาพิจารณาค่ะ";

        $keyboardButtons = [
            [['text' => '✅ อนุมัติ (Approve)', 'callback_data' => "mgrAction_approve_{$leaveRequestId}"]],
            [['text' => '❌ ไม่อนุมัติ (Reject)', 'callback_data' => "mgrAction_reject_{$leaveRequestId}"]]
        ];
        $replyMarkup = ['inline_keyboard' => $keyboardButtons];

        $sendResult = $this->telegramService->sendMessage($managerChatId, $messageToManager, $replyMarkup);
        if ($sendResult && isset($sendResult['ok']) && $sendResult['ok']) {
            log_message("Successfully sent notification to manager (ChatID: {$managerChatId}, ManagerEmpID: {$manager->id}) for request ID {$leaveRequestId}.");
        } else {
            log_message("Failed to send notification to manager (ChatID: {$managerChatId}, ManagerEmpID: {$manager->id}) for request ID {$leaveRequestId}. Response: ".json_encode($sendResult));
        }
    }
}