Files
2026-09-16 23:20:08 +07:00

477 lines
20 KiB
PHP

<?php
namespace app\Controllers;
use app\Models\PayrollModel;
use app\Models\IncomeMasterModel;
use app\Models\DeductionMasterModel;
use app\Models\HrPersonModel;
class ImportController extends Controller {
public function __construct() {
if (empty($_SESSION['user_id'])) {
$_SESSION['error'] = 'กรุณาเข้าสู่ระบบก่อนใช้งาน';
header('Location: ' . BASE_URL . '/login');
exit;
}
}
public function index() {
$payrollModel = new PayrollModel();
// Fetch all months for the dropdown
$periods = $payrollModel->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 รายการ)";
}
}