getAllPayrollPeriods(); $this->view('import/index', [ 'title' => 'นำเข้าข้อมูล | ' . APP_NAME, 'activeMenu' => 'import', 'periods' => $periods ]); } public function upload() { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { header('Location: ' . BASE_URL . '/import'); exit; } $salaryMonthId = $_POST['salary_month_id'] ?? null; $fileType = $_POST['file_type'] ?? 'csv'; if (empty($salaryMonthId) || empty($_FILES['csv_file']['tmp_name'])) { $_SESSION['error'] = 'กรุณาเลือกงวดเงินเดือนและไฟล์'; header('Location: ' . BASE_URL . '/import'); exit; } $file = $_FILES['csv_file']; if ($file['error'] !== UPLOAD_ERR_OK) { $_SESSION['error'] = 'เกิดข้อผิดพลาดในการอัปโหลดไฟล์'; header('Location: ' . BASE_URL . '/import'); exit; } $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); if ($fileType === 'cgd_txt' && $ext !== 'txt') { $_SESSION['error'] = 'ระบบต้องการไฟล์นามสกุล .txt สำหรับรูปแบบกรมบัญชีกลาง'; header('Location: ' . BASE_URL . '/import'); exit; } elseif ($fileType === 'csv' && $ext !== 'csv') { $_SESSION['error'] = 'รองรับเฉพาะไฟล์ .csv เท่านั้น'; header('Location: ' . BASE_URL . '/import'); exit; } $handle = fopen($file['tmp_name'], 'r'); if (!$handle) { $_SESSION['error'] = 'ไม่สามารถอ่านไฟล์ได้'; header('Location: ' . BASE_URL . '/import'); exit; } $incomeModel = new IncomeMasterModel(); $deductionModel = new DeductionMasterModel(); $payrollModel = new PayrollModel(); $payrollModel->beginTransaction(); try { if ($fileType === 'cgd_txt') { $this->processCGDFile($handle, $salaryMonthId, $payrollModel, $incomeModel, $deductionModel, $file['name']); } else { $this->processCSVFile($handle, $salaryMonthId, $payrollModel, $incomeModel, $deductionModel, $file['name']); } $payrollModel->commit(); fclose($handle); header('Location: ' . BASE_URL . '/import'); exit; } catch (\Exception $e) { $payrollModel->rollBack(); fclose($handle); $_SESSION['error'] = 'เกิดข้อผิดพลาดในการประมวลผล: ' . $e->getMessage(); header('Location: ' . BASE_URL . '/import'); exit; } } private function getIncomeMap($incomeModel) { $incomes = $incomeModel->getActiveIncomes(); $map = []; foreach ($incomes as $inc) { $map[$inc['code']] = $inc['id']; } return $map; } private function getDeductionMap($deductionModel) { $deductions = $deductionModel->getActiveDeductions(); $map = []; foreach ($deductions as $ded) { $map[$ded['code']] = $ded['id']; } return $map; } private function getOrCreateIncomeMaster($code, $name, $incomeModel, &$incomeMap) { if (isset($incomeMap[$code])) { return $incomeMap[$code]; } $incomeModel->createIncome([ 'code' => $code, 'name' => $name, 'is_taxable' => 1, 'is_active' => 1, 'display_order' => 99, 'fund_source' => 'CGD' ]); $incomeMap = $this->getIncomeMap($incomeModel); return $incomeMap[$code] ?? null; } private function getOrCreateDeductionMaster($code, $name, $deductionModel, &$deductionMap) { if (isset($deductionMap[$code])) { return $deductionMap[$code]; } $deductionModel->createDeduction([ 'code' => $code, 'name' => $name, 'is_taxable' => 1, 'is_active' => 1, 'display_order' => 99, 'fund_source' => 'CGD' ]); $deductionMap = $this->getDeductionMap($deductionModel); return $deductionMap[$code] ?? null; } private function parseCGDAmount($amountStr) { if (empty($amountStr) || strlen($amountStr) < 3) return 0; return (float) substr_replace($amountStr, ".", -2, 0); } private function processCGDFile($handle, $salaryMonthId, $payrollModel, $incomeModel, $deductionModel, $fileName) { $incomeMap = $this->getIncomeMap($incomeModel); $deductionMap = $this->getDeductionMap($deductionModel); $totalRecords = 0; $successRecords = 0; $errorRecords = 0; $profileModel = new \app\Models\EmployeeProfileModel(); $cgdIncomesStatic = [ 15 => 'เงินเดือนตกเบิก/ค่าจ้างประจําตกเบิก', 16 => 'เงิน ปตจ.', 17 => 'เงิน ปจต. ตกเบิก', 18 => 'พ.ข.อ./ตกเบิก', 19 => 'พ.ส.ร./ตกเบิก', 20 => 'พ.ค.ว./ตกเบิก', 21 => 'พ.ป.ผ./ตกเบิก', 22 => 'สปพ./ตกเบิก', 23 => 'ตปพ./ตกเบิก', 24 => 'ต.ข.ท.ปจต.', 25 => 'ต.ข.ท.ปจต. ตกเบิก', 26 => 'ต.ข.8-8ว.', 27 => 'ต.ข.8-8ว. ตกเบิก', 28 => 'ต.ด.ข.1-7/ตกเบิก, ต.ด.จ./ตกเบิก', 29 => 'ง.ต.พ.ข./ตกเบิก, ง.ต.พ.จ./ตกเบิก', 30 => 'ค่าเช่าบ้าน/ตกเบิก', 31 => 'ช่วยเหลือบุตร/ตกเบิก', 32 => 'การศึกษาบุตร/ตกเบิก', 33 => 'เงินรางวัล/เงินท้าทาย', 42 => 'เงินเพิ่มอื่นๆ', ]; $cgdDeductionsStatic = [ 44 => 'ภาษี/ตกเบิก', 45 => 'เงินกู้เพื่อที่อยู่อาศัย', 46 => 'ค่าหุ้น-เงินกู้สหกรณ์', 47 => 'เงินกู้เพื่อการศึกษา', 48 => 'กบข./ตกเบิก, กสจ./ตกเบิก', 49 => 'กบข.ส่วนเพิ่ม/ตกเบิก', 50 => 'ง.ก.บ.(ธอส.)', 51 => 'ง.ก.บ.(อส.)', 52 => 'ง.ก.ธ.', 53 => 'เงินกู้ ธพ.', 54 => 'ชดใช้ทางแพ่ง', 55 => 'เงินเรียกคืน', 56 => 'ค่าสาธารณูปโภค', 57 => 'เงินสวัสดิการสโมสร', 58 => 'ค่าฌาปนกิจ', 59 => 'งท. สงเคราะห์', 76 => 'เงินลด/หักอื่นๆ', ]; while (($line = fgets($handle)) !== false) { $line = trim($line); if (empty($line)) continue; // Convert encoding from TIS-620/Windows-874 to UTF-8 $utf8Line = @iconv('Windows-874', 'UTF-8//IGNORE', $line); if (!$utf8Line) { $utf8Line = @iconv('TIS-620', 'UTF-8//IGNORE', $line); } if (!$utf8Line) $utf8Line = $line; $data = explode('$', $utf8Line); if (count($data) < 70) continue; // Basic validation that it's a CGD row $totalRecords++; // Adjust to 1-based indexing for variables like f14, f15 to match user specs array_unshift($data, "dummy"); // Now $data[1] is Field 1 $nationalId = trim($data[3] ?? ''); $empCode = $nationalId; // Per decision, use national_id as employee_code if (empty($empCode)) { $errorRecords++; continue; } // --- 🔹 Fetch Names from external HR database (hosoffice_2566) --- $hrModel = new HrPersonModel(); $hrData = $hrModel->getPersonByNationalId($nationalId); if ($hrData) { // Get Prefix $prefix = trim($hrData['HR_PREFIX_NAME'] ?? ''); $firstName = trim($hrData['HR_FNAME'] ?? ''); $lastName = trim($hrData['HR_LNAME'] ?? ''); // Add prefix to first name if it exists (or keep separate, based on old code it had first_name and last_name) // Let's store prefix + first_name in first_name if (!empty($prefix) && strpos($firstName, $prefix) === false) { $firstName = $prefix . $firstName; } $position = trim(($hrData['HR_POSITION_NAME'] ?? '') . ' ' . ($hrData['HR_LEVEL_NAME'] ?? '')); $department = trim($hrData['HR_DEPARTMENT_SUB_SUB_NAME'] ?? ''); } else { // Fallback to text file if not found in HR DB (Column 5 and 6) $firstName = trim($data[5] ?? ''); $lastName = trim($data[6] ?? ''); $position = ''; $department = ''; } // ------------------------------------------------------------- // Extract Bank Details from CGD string (1-indexed due to array_unshift) $bankName = trim($data[10] ?? ''); $bankAccount = trim($data[13] ?? ''); if (!empty($bankName) || !empty($bankAccount)) { $profileModel->saveProfile($nationalId, $bankName, $bankAccount); } $baseSalaryAmount = $this->parseCGDAmount($data[14] ?? ''); $totalIncome = 0; $incomeEntries = []; // Base Salary (CGD) as an Income if ($baseSalaryAmount > 0) { $id = $this->getOrCreateIncomeMaster('CGD_INC_14', 'เงินเดือน/ค่าจ้าง (กรมบัญชีกลาง)', $incomeModel, $incomeMap); if ($id) { $totalIncome += $baseSalaryAmount; $incomeEntries[] = ['id' => $id, 'amount' => $baseSalaryAmount]; } } // Static incomes foreach ($cgdIncomesStatic as $index => $name) { $amt = $this->parseCGDAmount($data[$index] ?? ''); if ($amt > 0) { $code = "CGD_INC_" . $index; $id = $this->getOrCreateIncomeMaster($code, $name, $incomeModel, $incomeMap); if ($id) { $totalIncome += $amt; $incomeEntries[] = ['id' => $id, 'amount' => $amt]; } } } // Dynamic incomes (f34->f35, etc.) $dynamicIncomes = [ 34 => 35, 36 => 37, 38 => 39, 40 => 41 ]; foreach ($dynamicIncomes as $nameIdx => $valIdx) { $name = trim($data[$nameIdx] ?? ''); $amt = $this->parseCGDAmount($data[$valIdx] ?? ''); if ($amt > 0 && !empty($name)) { $code = "CGD_INC_" . $valIdx; $id = $this->getOrCreateIncomeMaster($code, $name, $incomeModel, $incomeMap); if ($id) { $totalIncome += $amt; $incomeEntries[] = ['id' => $id, 'amount' => $amt]; } } } $totalDeduction = 0; $deductionEntries = []; // Static deductions foreach ($cgdDeductionsStatic as $index => $name) { $amt = $this->parseCGDAmount($data[$index] ?? ''); if ($amt > 0) { $code = "CGD_DED_" . $index; $id = $this->getOrCreateDeductionMaster($code, $name, $deductionModel, $deductionMap); if ($id) { $totalDeduction += $amt; $deductionEntries[] = ['id' => $id, 'amount' => $amt]; } } } // Dynamic deductions (f60->f61, etc.) $dynamicDeductions = [ 60 => 61, 62 => 63, 64 => 65, 66 => 67, 68 => 69, 70 => 71, 72 => 73, 74 => 75 ]; foreach ($dynamicDeductions as $nameIdx => $valIdx) { $name = trim($data[$nameIdx] ?? ''); $amt = $this->parseCGDAmount($data[$valIdx] ?? ''); if ($amt > 0 && !empty($name)) { $code = "CGD_DED_" . $valIdx; $id = $this->getOrCreateDeductionMaster($code, $name, $deductionModel, $deductionMap); if ($id) { $totalDeduction += $amt; $deductionEntries[] = ['id' => $id, 'amount' => $amt]; } } } $netSalary = $totalIncome - $totalDeduction; $empData = [ 'salary_month_id' => $salaryMonthId, 'employee_code' => $empCode, 'national_id' => $nationalId, 'first_name' => $firstName, 'last_name' => $lastName, 'position' => $position, 'department' => $department, 'base_salary' => 0, // Hardcode base_salary to 0 since it's now in incomes 'total_income' => $totalIncome, 'total_deduction' => $totalDeduction, 'net_salary' => $netSalary ]; $empSalId = $payrollModel->insertEmployeeSalary($empData); foreach ($incomeEntries as $inc) { $payrollModel->insertSalaryIncome($empSalId, $inc['id'], $inc['amount']); } foreach ($deductionEntries as $ded) { $payrollModel->insertSalaryDeduction($empSalId, $ded['id'], $ded['amount']); } $successRecords++; } $payrollModel->updateSalaryMonthStatus($salaryMonthId, 'Imported'); $status = ($errorRecords > 0) ? 'partial' : 'success'; $payrollModel->logImportHistory($_SESSION['user_id'], $salaryMonthId, $fileName, $totalRecords, $successRecords, $errorRecords, $status); $_SESSION['success'] = "นำเข้าข้อมูลกรมบัญชีกลางสำเร็จ $successRecords รายการ (ผิดพลาด $errorRecords รายการ)"; } private function processCSVFile($handle, $salaryMonthId, $payrollModel, $incomeModel, $deductionModel, $fileName) { $header = fgetcsv($handle, 1000, ","); $incomeMap = $this->getIncomeMap($incomeModel); $deductionMap = $this->getDeductionMap($deductionModel); $colIdx = []; foreach ($header as $index => $colName) { $colName = trim($colName); $colIdx[$colName] = $index; } $requiredCols = ['Employee_Code', 'National_ID', 'First_Name', 'Last_Name', 'Base_Salary']; foreach ($requiredCols as $req) { if (!isset($colIdx[$req])) { throw new \Exception("ไฟล์ CSV ไม่ถูกต้อง ขาดคอลัมน์: $req"); } } $totalRecords = 0; $successRecords = 0; $errorRecords = 0; while (($data = fgetcsv($handle, 1000, ",")) !== false) { $totalRecords++; $empCode = trim($data[$colIdx['Employee_Code']]); if (empty($empCode)) { $errorRecords++; continue; } $baseSalary = (float)$data[$colIdx['Base_Salary']]; $totalIncome = 0; $totalDeduction = 0; $incomeEntries = []; foreach ($incomeMap as $code => $id) { if (isset($colIdx[$code])) { $amt = (float)$data[$colIdx[$code]]; if ($amt > 0) { $totalIncome += $amt; $incomeEntries[] = ['id' => $id, 'amount' => $amt]; } } } $deductionEntries = []; foreach ($deductionMap as $code => $id) { if (isset($colIdx[$code])) { $amt = (float)$data[$colIdx[$code]]; if ($amt > 0) { $totalDeduction += $amt; $deductionEntries[] = ['id' => $id, 'amount' => $amt]; } } } // Convert base salary to income if ($baseSalary > 0) { $id = $this->getOrCreateIncomeMaster('CGD_INC_14', 'เงินเดือน/ค่าจ้าง (กรมบัญชีกลาง)', $incomeModel, $incomeMap); if ($id) { $totalIncome += $baseSalary; $incomeEntries[] = ['id' => $id, 'amount' => $baseSalary]; } } $netSalary = $totalIncome - $totalDeduction; $empData = [ 'salary_month_id' => $salaryMonthId, 'employee_code' => $empCode, 'national_id' => trim($data[$colIdx['National_ID']]), 'first_name' => trim($data[$colIdx['First_Name']]), 'last_name' => trim($data[$colIdx['Last_Name']]), 'base_salary' => 0, // Hardcode base_salary to 0 'total_income' => $totalIncome, 'total_deduction' => $totalDeduction, 'net_salary' => $netSalary ]; $empSalId = $payrollModel->insertEmployeeSalary($empData); foreach ($incomeEntries as $inc) { $payrollModel->insertSalaryIncome($empSalId, $inc['id'], $inc['amount']); } foreach ($deductionEntries as $ded) { $payrollModel->insertSalaryDeduction($empSalId, $ded['id'], $ded['amount']); } $successRecords++; } $payrollModel->updateSalaryMonthStatus($salaryMonthId, 'Imported'); $status = ($errorRecords > 0) ? 'partial' : 'success'; $payrollModel->logImportHistory($_SESSION['user_id'], $salaryMonthId, $fileName, $totalRecords, $successRecords, $errorRecords, $status); $_SESSION['success'] = "นำเข้าข้อมูล CSV สำเร็จ $successRecords รายการ (ผิดพลาด $errorRecords รายการ)"; } }