Initial commit

This commit is contained in:
Porawit Dongwang
2026-09-16 23:20:08 +07:00
commit 0041668dbb
32577 changed files with 3687927 additions and 0 deletions
@@ -0,0 +1,68 @@
<?php
namespace app\Controllers;
use app\Models\UserModel;
use app\Models\UserLogModel;
class AuthController extends Controller {
public function index() {
// If already logged in, redirect to dashboard
if (isset($_SESSION['user_id'])) {
header('Location: ' . BASE_URL . '/dashboard');
exit;
}
// Render the login view
$this->view('auth/login', [
'title' => 'เข้าสู่ระบบ | ' . APP_NAME
]);
}
public function login() {
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
if (empty($username) || empty($password)) {
$_SESSION['error'] = 'กรุณากรอกข้อมูลให้ครบถ้วน';
header('Location: ' . BASE_URL . '/login');
exit;
}
$userModel = new UserModel();
$user = $userModel->getUserByUsername($username);
if ($user && password_verify($password, $user['password_hash'])) {
// Login success
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role_id'] = $user['role_id'];
$_SESSION['first_name'] = $user['first_name'];
$_SESSION['last_name'] = $user['last_name'];
$userModel->updateLastLogin($user['id']);
// Record Login Log
$logModel = new UserLogModel();
$logModel->logAction($user['id'], 'LOGIN', 'เข้าสู่ระบบสำเร็จ');
$_SESSION['success'] = 'เข้าสู่ระบบสำเร็จ ยินดีต้อนรับคุณ ' . htmlspecialchars($user['first_name']);
header('Location: ' . BASE_URL . '/dashboard');
exit;
} else {
// Login failed
$_SESSION['error'] = 'ชื่อผู้ใช้งานหรือรหัสผ่านไม่ถูกต้อง';
header('Location: ' . BASE_URL . '/login');
exit;
}
}
public function logout() {
session_destroy();
session_start(); // Start a new session for the success message
$_SESSION['success'] = 'ออกจากระบบสำเร็จ';
header('Location: ' . BASE_URL . '/login');
exit;
}
}
?>
@@ -0,0 +1,21 @@
<?php
namespace app\Controllers;
abstract class Controller {
/**
* Basic view rendering method
*/
protected function view($viewName, $data = []) {
// Extract data to variables
extract($data);
$viewPath = APP_ROOT . '/app/Views/' . $viewName . '.php';
if (file_exists($viewPath)) {
require_once $viewPath;
} else {
die("View does not exist: " . $viewName);
}
}
}
?>
@@ -0,0 +1,85 @@
<?php
namespace app\Controllers;
use app\Models\PayrollModel;
use app\Models\SystemSettingModel;
use PDO;
class DashboardController extends Controller {
public function __construct() {
if (empty($_SESSION['user_id'])) {
$_SESSION['error'] = 'กรุณาเข้าสู่ระบบก่อนใช้งาน';
header('Location: ' . BASE_URL . '/login');
exit;
}
}
public function index() {
$payrollModel = new PayrollModel();
// 1. Get latest payroll period
$periods = $payrollModel->getAllPayrollPeriods();
$latestPeriod = !empty($periods) ? $periods[0] : null;
// 2. Aggregate stats for the latest period
$totalEmployees = 0;
$totalSalary = 0;
if ($latestPeriod) {
$stats = $payrollModel->getPeriodStats($latestPeriod['id']);
$totalEmployees = $stats['total_emp'] ?? 0;
$totalSalary = $stats['total_net'] ?? 0;
}
// 3. For Chart: Get yearly summary
$settingModel = new SystemSettingModel();
$chartHistoryYears = (int)$settingModel->getSetting('chart_history_years', '3');
$baseYear = date('Y');
if ($latestPeriod && !empty($latestPeriod['year_no'])) {
$baseYear = $latestPeriod['year_no'];
}
$availableYears = [];
for ($i = 0; $i < $chartHistoryYears; $i++) {
$availableYears[] = $baseYear - $i;
}
$currentYear = $_GET['year'] ?? $baseYear;
if (!in_array($currentYear, $availableYears)) {
$currentYear = $baseYear;
}
$yearBE = $currentYear + 543;
$yearlyStats = $payrollModel->getYearlySummary($currentYear);
$chartLabels = ['ม.ค.', 'ก.พ.', 'มี.ค.', 'เม.ย.', 'พ.ค.', 'มิ.ย.', 'ก.ค.', 'ส.ค.', 'ก.ย.', 'ต.ค.', 'พ.ย.', 'ธ.ค.'];
$chartIncome = array_fill(0, 12, 0);
$chartDeduction = array_fill(0, 12, 0);
foreach ($yearlyStats as $monthStr => $data) {
$mIdx = (int)$monthStr - 1; // '01' -> 0, '12' -> 11
if ($mIdx >= 0 && $mIdx < 12) {
$chartIncome[$mIdx] = $data['income'];
$chartDeduction[$mIdx] = $data['deduction'];
}
}
$this->view('dashboard/index', [
'title' => 'Dashboard | ' . APP_NAME,
'activeMenu' => 'dashboard',
'latestPeriod' => $latestPeriod,
'totalEmployees' => $totalEmployees,
'totalSalary' => $totalSalary,
'currentYear' => $currentYear,
'yearBE' => $yearBE,
'availableYears' => $availableYears,
'chartLabels' => json_encode($chartLabels),
'chartIncome' => json_encode($chartIncome),
'chartDeduction' => json_encode($chartDeduction)
]);
}
}
?>
@@ -0,0 +1,45 @@
<?php
namespace app\Controllers;
use app\Models\HrPersonModel;
class HrController extends Controller {
public function __construct() {
if (empty($_SESSION['user_id'])) {
header('HTTP/1.1 401 Unauthorized');
exit;
}
}
public function image() {
$nationalId = isset($_GET['national_id']) ? $_GET['national_id'] : '';
if (empty($nationalId)) {
$this->outputDefaultImage();
return;
}
$hrModel = new HrPersonModel();
$imageData = $hrModel->getPersonImage($nationalId);
if (!$imageData || empty($imageData['HR_IMAGE'])) {
$this->outputDefaultImage();
return;
}
// Output image
header("Content-Type: image/jpeg");
header("Cache-Control: max-age=86400, public"); // Cache for 1 day
echo $imageData['HR_IMAGE'];
exit;
}
private function outputDefaultImage() {
// Output a 1x1 transparent PNG or an SVG avatar as fallback
header('Content-Type: image/svg+xml');
echo '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z" clip-rule="evenodd" /></svg>';
exit;
}
}
?>
@@ -0,0 +1,476 @@
<?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 รายการ)";
}
}
@@ -0,0 +1,162 @@
<?php
namespace app\Controllers;
use app\Models\PayrollModel;
use app\Models\HrPersonModel;
use app\Models\UserLogModel;
use app\Models\SystemSettingModel;
class KioskController extends Controller {
public function index() {
// Load the Kiosk UI
$this->view('kiosk/index');
}
public function search() {
header('Content-Type: application/json');
$nationalId = $_POST['national_id'] ?? null;
if (!$nationalId || strlen($nationalId) !== 13) {
echo json_encode(['status' => 'error', 'message' => 'รหัสบัตรประชาชนไม่ถูกต้อง']);
return;
}
$payrollModel = new PayrollModel();
$hrModel = new HrPersonModel();
// Check if person exists in HR DB
$person = $hrModel->getPersonByNationalId($nationalId);
$slips = $payrollModel->getAvailableSlipsByNationalId($nationalId);
$taxes = $payrollModel->getAvailableTaxesByNationalId($nationalId);
if (empty($slips) && empty($taxes)) {
echo json_encode(['status' => 'error', 'message' => 'ไม่พบข้อมูลเงินเดือนหรือใบเสียภาษีสำหรับรหัสบัตรประชาชนนี้']);
return;
}
$personName = $person ? trim($person['hr_fname'] . ' ' . $person['hr_lname']) : 'ไม่ทราบชื่อ';
echo json_encode([
'status' => 'success',
'data' => [
'name' => $personName,
'slips' => $slips,
'taxes' => $taxes
]
]);
}
public function printSlip() {
$nationalId = $_GET['national_id'] ?? null;
$month = $_GET['month'] ?? null;
$year = $_GET['year'] ?? null;
if (!$nationalId || !$month || !$year) {
die('ข้อมูลไม่ครบถ้วน');
}
$payrollModel = new PayrollModel();
$salary = $payrollModel->getSalaryByNationalId($nationalId, $month, $year);
if (!$salary) {
die('ไม่พบข้อมูลเงินเดือนของบุคคลนี้ในงวดที่เลือก');
}
// Log the action (null user_id)
$logModel = new UserLogModel();
$logModel->logAction(null, 'KIOSK_PRINT_SLIP', "ตู้ Kiosk: พิมพ์สลิปเงินเดือน งวด $month/$year สำหรับบัตรประชาชน $nationalId");
$incomes = $payrollModel->getEmployeeIncomes($salary['id']);
$deductions = $payrollModel->getEmployeeDeductions($salary['id']);
$hrModel = new HrPersonModel();
$hrData = $hrModel->getPersonByNationalId($nationalId);
if (!$hrData) {
$hrData = [];
}
require_once APP_ROOT . '/app/Views/reports/slip.php';
}
public function printSlipBulk() {
$nationalId = $_POST['national_id'] ?? null;
$periods = $_POST['periods'] ?? []; // format: 'MM-YYYY'
if (!$nationalId || empty($periods)) {
die('ข้อมูลไม่ครบถ้วน');
}
$payrollModel = new PayrollModel();
$hrModel = new HrPersonModel();
$hrData = $hrModel->getPersonByNationalId($nationalId);
if (!$hrData) {
$hrData = [];
}
$bankProfile = [
'bank_account_no' => $hrData['HR_BANK_ACCOUNT'] ?? '',
'bank_name' => '' // Not provided in HR data directly without another query
];
$bulkData = [];
foreach ($periods as $period) {
list($month, $year) = explode('-', $period);
$salary = $payrollModel->getSalaryByNationalId($nationalId, $month, $year);
if ($salary) {
$bulkData[] = [
'salary' => $salary,
'hrData' => $hrData,
'bankProfile' => $bankProfile,
'incomes' => $payrollModel->getEmployeeIncomes($salary['id']),
'deductions' => $payrollModel->getEmployeeDeductions($salary['id']),
'month' => $month,
'year' => $year
];
}
}
if (empty($bulkData)) {
die('ไม่พบข้อมูลเงินเดือนสำหรับช่วงที่เลือก');
}
// Log the action
$logModel = new UserLogModel();
$logModel->logAction(null, 'KIOSK_PRINT_SLIP', "ตู้ Kiosk: พิมพ์สลิปเงินเดือนแบบกลุ่ม (" . count($periods) . " เดือน) สำหรับบัตรประชาชน $nationalId");
require_once APP_ROOT . '/app/Views/kiosk/slip_bulk.php';
}
public function printTax() {
$nationalId = $_GET['national_id'] ?? null;
$year = $_GET['year'] ?? null;
if (!$nationalId || !$year) {
die('ข้อมูลไม่ครบถ้วน');
}
$payrollModel = new PayrollModel();
$yearlySummary = $payrollModel->getYearlyTaxSummary($nationalId, $year);
if (!$yearlySummary || $yearlySummary['total_income'] == 0) {
die('ไม่พบข้อมูลรายได้ในปีที่เลือก');
}
// Log the action
$logModel = new UserLogModel();
$logModel->logAction(null, 'KIOSK_PRINT_TAX', "ตู้ Kiosk: พิมพ์ใบ 50 ทวิ ปี $year สำหรับบัตรประชาชน $nationalId");
$hrModel = new HrPersonModel();
$hrData = $hrModel->getPersonByNationalId($nationalId);
if (!$hrData) {
$hrData = [];
}
$settingModel = new SystemSettingModel();
$orgTaxId = $settingModel->getSetting('org_tax_id', '');
require_once APP_ROOT . '/app/Views/reports/tax.php';
}
}
?>
@@ -0,0 +1,20 @@
<?php
namespace app\Controllers;
class ManualController extends Controller {
public function __construct() {
if (empty($_SESSION['user_id'])) {
header('Location: ' . BASE_URL . '/login');
exit;
}
}
public function index() {
$this->view('manual/index', [
'title' => 'คู่มือการใช้งาน | ' . APP_NAME,
'activeMenu' => 'manual'
]);
}
}
?>
@@ -0,0 +1,464 @@
<?php
namespace app\Controllers;
use app\Models\PayrollModel;
use app\Models\EmployeeProfileModel;
class PayrollController extends Controller {
public function __construct() {
if (empty($_SESSION['user_id'])) {
$_SESSION['error'] = 'กรุณาเข้าสู่ระบบก่อนใช้งาน';
header('Location: ' . BASE_URL . '/login');
exit;
}
}
public function index() {
$payrollModel = new PayrollModel();
$periods = $payrollModel->getAllPayrollPeriods();
$this->view('payroll/index', [
'title' => 'จัดการงวดเงินเดือน | ' . APP_NAME,
'activeMenu' => 'payroll',
'periods' => $periods
]);
}
public function store() {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: ' . BASE_URL . '/payroll');
exit;
}
$yearNo = isset($_POST['year_no']) ? (int)$_POST['year_no'] : 0;
$monthNo = isset($_POST['month_no']) ? (int)$_POST['month_no'] : 0;
if ($yearNo < 2000 || $monthNo < 1 || $monthNo > 12) {
$_SESSION['error'] = 'ข้อมูลปีหรือเดือนไม่ถูกต้อง';
header('Location: ' . BASE_URL . '/payroll');
exit;
}
$payrollModel = new PayrollModel();
if ($payrollModel->checkPeriodExists($yearNo, $monthNo)) {
$_SESSION['error'] = 'งวดเงินเดือนนี้มีอยู่ในระบบแล้ว';
header('Location: ' . BASE_URL . '/payroll');
exit;
}
if ($payrollModel->createPayrollPeriod($yearNo, $monthNo)) {
$_SESSION['success'] = "สร้างงวดเงินเดือน $monthNo/$yearNo สำเร็จ (สถานะ: Draft)";
} else {
$_SESSION['error'] = 'เกิดข้อผิดพลาดในการสร้างงวดเงินเดือน';
}
header('Location: ' . BASE_URL . '/payroll');
exit;
}
public function updateStatus() {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = $_POST['id'] ?? null;
$status = $_POST['status'] ?? null;
if ($id && $status) {
$payrollModel = new PayrollModel();
$success = $payrollModel->updateSalaryMonthStatus($id, $status);
if ($success) {
$_SESSION['success'] = "อัปเดตสถานะเป็น $status สำเร็จ ข้อมูลจะแสดงที่ตู้ Kiosk แล้ว";
} else {
$_SESSION['error'] = 'เกิดข้อผิดพลาดในการอัปเดตสถานะ';
}
} else {
$_SESSION['error'] = 'ข้อมูลไม่ครบถ้วน';
}
header('Location: ' . BASE_URL . '/payroll/details/' . $id);
exit;
}
}
public function copyPreviousMonth() {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: ' . BASE_URL . '/payroll');
exit;
}
$currentMonthId = isset($_POST['salary_month_id']) ? (int)$_POST['salary_month_id'] : 0;
if (!$currentMonthId) {
$_SESSION['error'] = 'ข้อมูลไม่ถูกต้อง';
header('Location: ' . BASE_URL . '/payroll');
exit;
}
$payrollModel = new PayrollModel();
$currentPeriod = $payrollModel->getSalaryMonthById($currentMonthId);
if (!$currentPeriod) {
$_SESSION['error'] = 'ไม่พบงวดเงินเดือนที่ระบุ';
header('Location: ' . BASE_URL . '/payroll');
exit;
}
if ($currentPeriod['status'] !== 'Draft') {
$_SESSION['error'] = 'ไม่สามารถเพิ่มข้อมูลได้เนื่องจากงวดเงินเดือนถูกล็อคแล้ว';
header('Location: ' . BASE_URL . '/payroll/details?id=' . $currentMonthId);
exit;
}
// Find previous month
$prevMonthId = $payrollModel->getPreviousSalaryMonth($currentPeriod['year_no'], $currentPeriod['month_no']);
if (!$prevMonthId) {
$_SESSION['error'] = 'ไม่พบข้อมูลงวดเงินเดือนของเดือนก่อนหน้าในระบบ';
header('Location: ' . BASE_URL . '/payroll/details?id=' . $currentMonthId);
exit;
}
$result = $payrollModel->copyFromPreviousMonth($currentMonthId, $prevMonthId);
if ($result !== false) {
$copiedCount = $result['copied'];
$mergedCount = $result['merged'];
if ($copiedCount > 0 || $mergedCount > 0) {
$_SESSION['success'] = "คัดลอกข้อมูลสำเร็จ: เพิ่มพนักงานใหม่ $copiedCount คน, อัปเดตรายการรับจ่ายของโรงพยาบาล $mergedCount คน";
} else {
$_SESSION['success'] = 'ไม่มีข้อมูลที่ต้องคัดลอกเพิ่มเติม (ข้อมูลถูกคัดลอกมาหมดแล้ว)';
}
} else {
$_SESSION['error'] = 'เกิดข้อผิดพลาดในการคัดลอกข้อมูล';
}
header('Location: ' . BASE_URL . '/payroll/details?id=' . $currentMonthId);
exit;
}
public function clearData() {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: ' . BASE_URL . '/payroll');
exit;
}
$salaryMonthId = isset($_POST['salary_month_id']) ? (int)$_POST['salary_month_id'] : 0;
if ($salaryMonthId <= 0) {
$_SESSION['error'] = 'ไม่พบข้อมูลงวดเงินเดือนที่ต้องการล้าง';
header('Location: ' . BASE_URL . '/payroll');
exit;
}
$payrollModel = new PayrollModel();
$period = $payrollModel->getSalaryMonthById($salaryMonthId);
if (!$period) {
$_SESSION['error'] = 'ไม่พบข้อมูลงวดเงินเดือนในระบบ';
header('Location: ' . BASE_URL . '/payroll');
exit;
}
if ($period['status'] === 'Locked' || $period['status'] === 'Published') {
$_SESSION['error'] = 'ไม่สามารถล้างข้อมูลได้เนื่องจากงวดเงินเดือนถูกล็อกหรือประกาศใช้แล้ว';
header('Location: ' . BASE_URL . '/payroll');
exit;
}
if ($payrollModel->clearDataForPeriod($salaryMonthId)) {
$_SESSION['success'] = 'ล้างข้อมูลการนำเข้าสำหรับงวดเงินเดือนนี้เรียบร้อยแล้ว';
} else {
$_SESSION['error'] = 'เกิดข้อผิดพลาดในการล้างข้อมูล';
}
header('Location: ' . BASE_URL . '/payroll');
exit;
}
public function details() {
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if ($id <= 0) {
$_SESSION['error'] = 'ไม่พบข้อมูลงวดเงินเดือน';
header('Location: ' . BASE_URL . '/payroll');
exit;
}
$payrollModel = new PayrollModel();
$period = $payrollModel->getSalaryMonthById($id);
if (!$period) {
$_SESSION['error'] = 'ไม่พบข้อมูลงวดเงินเดือนในระบบ';
header('Location: ' . BASE_URL . '/payroll');
exit;
}
$employees = $payrollModel->getEmployeesBySalaryMonthId($id);
$stats = $payrollModel->getPeriodStats($id);
// Map dynamic HR data
$cids = array_column($employees, 'national_id');
$hrModel = new \app\Models\HrPersonModel();
$hrPersons = $hrModel->getPersonsByNationalIds($cids);
foreach ($employees as &$emp) {
$cid = $emp['national_id'];
if (isset($hrPersons[$cid])) {
$hr = $hrPersons[$cid];
$prefix = trim($hr['HR_PREFIX_NAME'] ?? '');
$firstName = trim($hr['HR_FNAME'] ?? '');
if (!empty($prefix) && strpos($firstName, $prefix) === false) {
$firstName = $prefix . $firstName;
}
$emp['first_name'] = $firstName;
$emp['last_name'] = trim($hr['HR_LNAME'] ?? '');
$emp['position'] = trim(($hr['HR_POSITION_NAME'] ?? '') . ' ' . ($hr['HR_LEVEL_NAME'] ?? ''));
$emp['department'] = trim($hr['HR_DEPARTMENT_SUB_SUB_NAME'] ?? '');
}
}
unset($emp);
$this->view('payroll/details', [
'title' => 'รายละเอียดงวดเงินเดือน | ' . APP_NAME,
'activeMenu' => 'payroll',
'period' => $period,
'employees' => $employees,
'stats' => $stats
]);
}
public function addEmployee() {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: ' . BASE_URL . '/payroll');
exit;
}
$salaryMonthId = isset($_POST['salary_month_id']) ? (int)$_POST['salary_month_id'] : 0;
$nationalId = trim($_POST['national_id'] ?? '');
if ($salaryMonthId <= 0 || empty($nationalId)) {
$_SESSION['error'] = 'ข้อมูลไม่ครบถ้วน กรุณากรอกเลขบัตรประชาชน';
header('Location: ' . BASE_URL . '/payroll/details?id=' . $salaryMonthId);
exit;
}
// 1. Fetch from HR DB
$hrModel = new \app\Models\HrPersonModel();
$hrData = $hrModel->getPersonByNationalId($nationalId);
if (!$hrData) {
$_SESSION['error'] = 'ไม่พบข้อมูลบุคคลนี้ในฐานข้อมูล HR (hosoffice_2566)';
header('Location: ' . BASE_URL . '/payroll/details?id=' . $salaryMonthId);
exit;
}
// 2. Prepare Data
$prefix = trim($hrData['HR_PREFIX_NAME'] ?? '');
$firstName = trim($hrData['HR_FNAME'] ?? '');
$lastName = trim($hrData['HR_LNAME'] ?? '');
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'] ?? '');
$data = [
'salary_month_id' => $salaryMonthId,
'employee_code' => $nationalId,
'national_id' => $nationalId,
'first_name' => $firstName,
'last_name' => $lastName,
'position' => $position,
'department' => $department,
'base_salary' => 0,
'total_income' => 0,
'total_deduction' => 0,
'net_salary' => 0
];
// 3. Save
$payrollModel = new PayrollModel();
$payrollModel->insertEmployeeSalary($data);
$_SESSION['success'] = 'เพิ่มรายชื่อพนักงานสำเร็จ: ' . $firstName . ' ' . $lastName;
header('Location: ' . BASE_URL . '/payroll/details?id=' . $salaryMonthId);
exit;
}
public function deleteEmployee() {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: ' . BASE_URL . '/payroll');
exit;
}
$id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
$salaryMonthId = isset($_POST['salary_month_id']) ? (int)$_POST['salary_month_id'] : 0;
if ($id <= 0 || $salaryMonthId <= 0) {
$_SESSION['error'] = 'ข้อมูลไม่ถูกต้อง';
header('Location: ' . BASE_URL . '/payroll/details?id=' . $salaryMonthId);
exit;
}
$payrollModel = new PayrollModel();
if ($payrollModel->deleteEmployeeSalary($id)) {
$_SESSION['success'] = 'ลบข้อมูลพนักงานออกจากงวดนี้สำเร็จ';
} else {
$_SESSION['error'] = 'เกิดข้อผิดพลาดในการลบข้อมูล';
}
header('Location: ' . BASE_URL . '/payroll/details?id=' . $salaryMonthId);
exit;
}
public function editEmployee() {
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if ($id <= 0) {
$_SESSION['error'] = 'ไม่พบข้อมูลพนักงาน';
header('Location: ' . BASE_URL . '/payroll');
exit;
}
$payrollModel = new PayrollModel();
$employee = $payrollModel->getEmployeeSalaryById($id);
if (!$employee) {
$_SESSION['error'] = 'ไม่พบข้อมูลพนักงานในระบบ';
header('Location: ' . BASE_URL . '/payroll');
exit;
}
$period = $payrollModel->getSalaryMonthById($employee['salary_month_id']);
if ($period['status'] === 'Locked' || $period['status'] === 'Published') {
$_SESSION['error'] = 'งวดเงินเดือนถูกล็อกแล้ว ไม่สามารถแก้ไขข้อมูลได้';
header('Location: ' . BASE_URL . '/payroll/details?id=' . $employee['salary_month_id']);
exit;
}
$incomes = $payrollModel->getEmployeeIncomes($id);
$deductions = $payrollModel->getEmployeeDeductions($id);
// Fetch HR data dynamically
$hrModel = new \app\Models\HrPersonModel();
$hrData = $hrModel->getPersonByNationalId($employee['national_id']);
if ($hrData) {
$prefix = trim($hrData['HR_PREFIX_NAME'] ?? '');
$firstName = trim($hrData['HR_FNAME'] ?? '');
if (!empty($prefix) && strpos($firstName, $prefix) === false) {
$firstName = $prefix . $firstName;
}
$employee['first_name'] = $firstName;
$employee['last_name'] = trim($hrData['HR_LNAME'] ?? '');
$employee['position'] = trim(($hrData['HR_POSITION_NAME'] ?? '') . ' ' . ($hrData['HR_LEVEL_NAME'] ?? ''));
$employee['department'] = trim($hrData['HR_DEPARTMENT_SUB_SUB_NAME'] ?? '');
}
// Fetch master data for dropdowns
$incomeModel = new \app\Models\IncomeMasterModel();
$masterIncomes = $incomeModel->getActiveIncomes();
$deductionModel = new \app\Models\DeductionMasterModel();
$masterDeductions = $deductionModel->getActiveDeductions();
// Fetch bank profile
$profileModel = new EmployeeProfileModel();
$profile = $profileModel->getProfile($employee['national_id']);
if ($profile) {
$employee['bank_name'] = $profile['bank_name'];
$employee['bank_account'] = $profile['bank_account'];
} else {
$employee['bank_name'] = 'ธนาคารกรุงไทย จำกัด(มหาชน)';
$employee['bank_account'] = '';
}
$this->view('payroll/employee_edit', [
'title' => 'แก้ไขข้อมูลเงินเดือน | ' . APP_NAME,
'activeMenu' => 'payroll',
'employee' => $employee,
'period' => $period,
'incomes' => $incomes,
'deductions' => $deductions,
'masterIncomes' => $masterIncomes,
'masterDeductions' => $masterDeductions
]);
}
public function updateEmployee() {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: ' . BASE_URL . '/payroll');
exit;
}
$id = isset($_POST['employee_salary_id']) ? (int)$_POST['employee_salary_id'] : 0;
$salaryMonthId = isset($_POST['salary_month_id']) ? (int)$_POST['salary_month_id'] : 0;
if ($id <= 0 || $salaryMonthId <= 0) {
$_SESSION['error'] = 'ข้อมูลไม่ถูกต้อง';
header('Location: ' . BASE_URL . '/payroll');
exit;
}
$payrollModel = new PayrollModel();
$period = $payrollModel->getSalaryMonthById($salaryMonthId);
if ($period['status'] === 'Locked' || $period['status'] === 'Published') {
$_SESSION['error'] = 'งวดเงินเดือนถูกล็อกแล้ว ไม่สามารถแก้ไขข้อมูลได้';
header('Location: ' . BASE_URL . '/payroll/details?id=' . $salaryMonthId);
exit;
}
$totalIncome = 0.0;
$incomesData = [];
if (isset($_POST['income_id']) && is_array($_POST['income_id'])) {
for ($i = 0; $i < count($_POST['income_id']); $i++) {
$incId = (int)$_POST['income_id'][$i];
$amt = (float)str_replace(',', '', $_POST['income_amount'][$i]);
if ($incId > 0 && $amt > 0) {
$incomesData[] = ['id' => $incId, 'amount' => $amt];
$totalIncome += $amt;
}
}
}
$totalDeduction = 0.0;
$deductionsData = [];
if (isset($_POST['deduction_id']) && is_array($_POST['deduction_id'])) {
for ($i = 0; $i < count($_POST['deduction_id']); $i++) {
$dedId = (int)$_POST['deduction_id'][$i];
$amt = (float)str_replace(',', '', $_POST['deduction_amount'][$i]);
if ($dedId > 0 && $amt > 0) {
$deductionsData[] = ['id' => $dedId, 'amount' => $amt];
$totalDeduction += $amt;
}
}
}
$netSalary = $totalIncome - $totalDeduction;
// Save Profile (Bank info)
$bankName = trim($_POST['bank_name'] ?? '');
$bankAccount = trim($_POST['bank_account'] ?? '');
$empRecord = $payrollModel->getEmployeeSalaryById($id);
if ($empRecord) {
$profileModel = new EmployeeProfileModel();
$profileModel->saveProfile($empRecord['national_id'], $bankName, $bankAccount);
}
if ($payrollModel->updateEmployeeSalaryDetails($id, 0, $incomesData, $deductionsData, $totalIncome, $totalDeduction, $netSalary)) {
$_SESSION['success'] = 'อัปเดตข้อมูลเงินเดือนเรียบร้อยแล้ว';
} else {
$_SESSION['error'] = 'เกิดข้อผิดพลาดในการบันทึกข้อมูล';
}
header('Location: ' . BASE_URL . '/payroll/details?id=' . $salaryMonthId);
exit;
}
}
?>
@@ -0,0 +1,71 @@
<?php
namespace app\Controllers;
use app\Models\UserModel;
use app\Models\UserLogModel;
class ProfileController extends Controller {
public function index() {
$userModel = new UserModel();
$user = $userModel->getUserById($_SESSION['user_id']);
$roleName = $user['role_id'] == 1 ? 'ผู้ดูแลระบบ (Admin)' : 'ผู้ใช้งานทั่วไป (User)';
$this->view('profile/index', [
'title' => 'ข้อมูลส่วนตัว | ' . APP_NAME,
'user' => $user,
'roleName' => $roleName
]);
}
public function updatePassword() {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$currentPassword = $_POST['current_password'] ?? '';
$newPassword = $_POST['new_password'] ?? '';
$confirmPassword = $_POST['confirm_password'] ?? '';
if (empty($currentPassword) || empty($newPassword) || empty($confirmPassword)) {
$_SESSION['error'] = 'กรุณากรอกข้อมูลให้ครบถ้วน';
header('Location: ' . BASE_URL . '/profile');
exit;
}
if ($newPassword !== $confirmPassword) {
$_SESSION['error'] = 'รหัสผ่านใหม่ไม่ตรงกัน';
header('Location: ' . BASE_URL . '/profile');
exit;
}
if (strlen($newPassword) < 6) {
$_SESSION['error'] = 'รหัสผ่านใหม่ต้องมีความยาวอย่างน้อย 6 ตัวอักษร';
header('Location: ' . BASE_URL . '/profile');
exit;
}
$userModel = new UserModel();
$user = $userModel->getUserById($_SESSION['user_id']);
if (!$user || !password_verify($currentPassword, $user['password_hash'])) {
$_SESSION['error'] = 'รหัสผ่านปัจจุบันไม่ถูกต้อง';
header('Location: ' . BASE_URL . '/profile');
exit;
}
try {
$userModel->updatePassword($_SESSION['user_id'], $newPassword);
$logModel = new UserLogModel();
$logModel->logAction($_SESSION['user_id'], 'UPDATE_USER', 'เปลี่ยนรหัสผ่านของตนเองผ่านหน้าข้อมูลส่วนตัว');
$_SESSION['success'] = 'เปลี่ยนรหัสผ่านเรียบร้อยแล้ว';
} catch (\Exception $e) {
$_SESSION['error'] = 'เกิดข้อผิดพลาด ไม่สามารถเปลี่ยนรหัสผ่านได้';
}
header('Location: ' . BASE_URL . '/profile');
exit;
}
}
}
?>
@@ -0,0 +1,427 @@
<?php
namespace app\Controllers;
use app\Models\PayrollModel;
use app\Models\HrPersonModel;
use app\Models\IncomeMasterModel;
use app\Models\DeductionMasterModel;
use app\Models\OfficerTypeModel;
class ReportController extends Controller {
public function __construct() {
if (empty($_SESSION['user_id'])) {
$_SESSION['error'] = 'กรุณาเข้าสู่ระบบก่อนใช้งาน';
header('Location: ' . BASE_URL . '/login');
exit;
}
}
public function index() {
$payrollModel = new PayrollModel();
// Get filter inputs
$month = $_GET['month'] ?? date('m');
$year = $_GET['year'] ?? date('Y');
// Fetch payroll data for the selected month/year
$payrollData = $payrollModel->getSalariesByPeriod($month, $year);
// Fetch matching HR data
$hrModel = new HrPersonModel();
$nationalIds = array_column($payrollData, 'national_id');
$hrData = $hrModel->getPersonsByNationalIds($nationalIds);
// Combine data
foreach ($payrollData as &$row) {
$nid = $row['national_id'];
if (isset($hrData[$nid])) {
$row['hr_fname'] = $hrData[$nid]['HR_FNAME'];
$row['hr_lname'] = $hrData[$nid]['HR_LNAME'];
$row['hr_position'] = $hrData[$nid]['HR_POSITION_NAME'];
$row['hr_department'] = $hrData[$nid]['HR_DEPARTMENT_SUB_SUB_NAME'];
}
}
$this->view('reports/index', [
'title' => 'ระบบสืบค้นและพิมพ์สลิปเงินเดือน | ' . APP_NAME,
'activeMenu' => 'reports',
'month' => $month,
'year' => $year,
'payrollData' => $payrollData
]);
}
public function printSlip() {
$nationalId = $_GET['national_id'] ?? null;
$month = $_GET['month'] ?? null;
$year = $_GET['year'] ?? null;
if (!$nationalId || !$month || !$year) {
die('ข้อมูลไม่ครบถ้วน');
}
$payrollModel = new PayrollModel();
$salary = $payrollModel->getSalaryByNationalId($nationalId, $month, $year);
if (!$salary) {
die('ไม่พบข้อมูลเงินเดือนของบุคคลนี้ในงวดที่เลือก');
}
$incomes = $payrollModel->getEmployeeIncomes($salary['id']);
$deductions = $payrollModel->getEmployeeDeductions($salary['id']);
$hrModel = new HrPersonModel();
$hrData = $hrModel->getPersonByNationalId($nationalId);
$profileModel = new \app\Models\EmployeeProfileModel();
$bankProfile = $profileModel->getProfile($nationalId);
$settingModel = new \app\Models\SystemSettingModel();
$signatureName = $settingModel->getSetting('signature_name', '( นางณัฐฐิณี เรืองทอง )');
$signaturePosition = $settingModel->getSetting('signature_position', 'นักวิชาการเงินและบัญชี');
$signatureScale = $settingModel->getSetting('signature_scale', '100');
$slipFontSize = $settingModel->getSetting('slip_font_size', '11pt');
$signaturePosX = $settingModel->getSetting('signature_pos_x', '0');
$signaturePosY = $settingModel->getSetting('signature_pos_y', '0');
$this->view('reports/slip', [
'salary' => $salary,
'incomes' => $incomes,
'deductions' => $deductions,
'hrData' => $hrData,
'bankProfile' => $bankProfile,
'signatureName' => $signatureName,
'signaturePosition' => $signaturePosition,
'signatureScale' => $signatureScale,
'slipFontSize' => $slipFontSize,
'signaturePosX' => $signaturePosX,
'signaturePosY' => $signaturePosY,
'month' => $month,
'year' => $year
]);
}
public function printTax() {
$nationalId = $_GET['national_id'] ?? null;
$year = $_GET['year'] ?? null;
if (!$nationalId || !$year) {
die('ข้อมูลไม่ครบถ้วน');
}
$payrollModel = new PayrollModel();
$yearlySummary = $payrollModel->getYearlyTaxSummary($nationalId, $year);
if (!$yearlySummary || $yearlySummary['total_income'] == 0) {
die('ไม่พบข้อมูลรายได้ในปีที่เลือก');
}
$hrModel = new HrPersonModel();
$hrData = $hrModel->getPersonByNationalId($nationalId);
$settingModel = new \app\Models\SystemSettingModel();
$orgTaxId = $settingModel->getSetting('org_tax_id', '');
$taxSignatureName = $settingModel->getSetting('tax_signature_name', '');
$taxSignaturePosition = $settingModel->getSetting('tax_signature_position', '');
$taxSignatureScale = $settingModel->getSetting('tax_signature_scale', '100');
$taxSignaturePosX = $settingModel->getSetting('tax_signature_pos_x', '0');
$taxSignaturePosY = $settingModel->getSetting('tax_signature_pos_y', '0');
$taxFontSize = $settingModel->getSetting('tax_font_size', '11pt');
$orgAddress = $settingModel->getSetting('org_address', 'ต.อ่างทอง อ.เกาะสมุย จ.สุราษฎร์ธานี 84140');
$taxPayeeAddress = $settingModel->getSetting('tax_payee_address', 'ต.อ่างทอง อ.เกาะสมุย จ.สุราษฎร์ธานี 84140');
$this->view('reports/tax', [
'yearlySummary' => $yearlySummary,
'hrData' => $hrData,
'year' => $year,
'nationalId' => $nationalId,
'orgTaxId' => $orgTaxId,
'taxSignatureName' => $taxSignatureName,
'taxSignaturePosition' => $taxSignaturePosition,
'taxSignatureScale' => $taxSignatureScale,
'taxSignaturePosX' => $taxSignaturePosX,
'taxSignaturePosY' => $taxSignaturePosY,
'taxFontSize' => $taxFontSize,
'orgAddress' => $orgAddress,
'taxPayeeAddress' => $taxPayeeAddress
]);
}
public function printTaxBulk() {
$nationalIds = $_POST['national_ids'] ?? [];
$year = $_POST['year'] ?? null;
$groupByLetter = isset($_POST['group_by_letter']) && $_POST['group_by_letter'] == '1';
if (empty($nationalIds) || !$year) {
die('ข้อมูลไม่ครบถ้วน กรุณาเลือกพนักงานอย่างน้อย 1 รายการ');
}
$payrollModel = new PayrollModel();
$hrModel = new HrPersonModel();
$bulkData = [];
foreach ($nationalIds as $nationalId) {
$yearlySummary = $payrollModel->getYearlyTaxSummary($nationalId, $year);
if ($yearlySummary && $yearlySummary['total_income'] > 0) {
$hrData = $hrModel->getPersonByNationalId($nationalId);
$bulkData[] = [
'nationalId' => $nationalId,
'yearlySummary' => $yearlySummary,
'hrData' => $hrData
];
}
}
if (empty($bulkData)) {
die('ไม่พบข้อมูลรายได้ในปีที่เลือกของพนักงานที่เลือกทั้งหมด');
}
// Sort data by First Name if grouping by letter is enabled
if ($groupByLetter) {
usort($bulkData, function($a, $b) {
$nameA = $a['hrData']['HR_FNAME'] ?? '';
$nameB = $b['hrData']['HR_FNAME'] ?? '';
return strcmp($nameA, $nameB);
});
}
$settingModel = new \app\Models\SystemSettingModel();
$orgTaxId = $settingModel->getSetting('org_tax_id', '');
$taxSignatureName = $settingModel->getSetting('tax_signature_name', '');
$taxSignaturePosition = $settingModel->getSetting('tax_signature_position', '');
$taxSignatureScale = $settingModel->getSetting('tax_signature_scale', '100');
$taxSignaturePosX = $settingModel->getSetting('tax_signature_pos_x', '0');
$taxSignaturePosY = $settingModel->getSetting('tax_signature_pos_y', '0');
$taxFontSize = $settingModel->getSetting('tax_font_size', '11pt');
$orgAddress = $settingModel->getSetting('org_address', 'ต.อ่างทอง อ.เกาะสมุย จ.สุราษฎร์ธานี 84140');
$taxPayeeAddress = $settingModel->getSetting('tax_payee_address', 'ต.อ่างทอง อ.เกาะสมุย จ.สุราษฎร์ธานี 84140');
$this->view('reports/tax_bulk', [
'bulkData' => $bulkData,
'year' => $year,
'groupByLetter' => $groupByLetter,
'orgTaxId' => $orgTaxId,
'taxSignatureName' => $taxSignatureName,
'taxSignaturePosition' => $taxSignaturePosition,
'taxSignatureScale' => $taxSignatureScale,
'taxSignaturePosX' => $taxSignaturePosX,
'taxSignaturePosY' => $taxSignaturePosY,
'taxFontSize' => $taxFontSize,
'orgAddress' => $orgAddress,
'taxPayeeAddress' => $taxPayeeAddress
]);
}
public function salaryReport() {
$payrollModel = new PayrollModel();
$hrModel = new HrPersonModel();
$officerTypeModel = new OfficerTypeModel();
$month = $_GET['month'] ?? date('m');
$year = $_GET['year'] ?? date('Y');
$typeId = $_GET['type_id'] ?? '';
$officerTypes = $officerTypeModel->getActive();
$selectedTypeName = '';
if ($typeId) {
foreach ($officerTypes as $type) {
if ($type['id'] == $typeId) {
$selectedTypeName = $type['name'];
break;
}
}
}
$payrollData = $payrollModel->getSalariesByPeriod($month, $year);
$nationalIds = array_column($payrollData, 'national_id');
$hrData = $hrModel->getPersonsByNationalIds($nationalIds);
$reportData = [];
foreach ($payrollData as $row) {
$nid = $row['national_id'];
if (isset($hrData[$nid])) {
$personType = $hrData[$nid]['HR_PERSON_TYPE_NAME'] ?? '';
// Filter by type if selected
if ($typeId && $personType !== $selectedTypeName) {
continue;
}
$row['hr_prefix'] = $hrData[$nid]['HR_PREFIX_NAME'] ?? '';
$row['hr_fname'] = $hrData[$nid]['HR_FNAME'] ?? '';
$row['hr_lname'] = $hrData[$nid]['HR_LNAME'] ?? '';
$row['hr_position'] = $hrData[$nid]['HR_POSITION_NAME'] ?? '';
$row['hr_department'] = $hrData[$nid]['HR_DEPARTMENT_SUB_SUB_NAME'] ?? '';
$row['person_type'] = $personType;
// Fetch incomes and deductions to display sums or details if needed
// But for performance, we can just use the cached totals if available.
// Wait, if we need specific columns for Excel, we should fetch details.
// In this case, we fetch them here or in export mode.
$reportData[] = $row;
}
}
// Sort by first name (ignoring prefix)
usort($reportData, function($a, $b) {
return strcmp($a['hr_fname'], $b['hr_fname']);
});
$this->view('reports/salary_report', [
'title' => 'รายงานเงินเดือน | ' . APP_NAME,
'activeMenu' => 'report_salary',
'month' => $month,
'year' => $year,
'typeId' => $typeId,
'officerTypes' => $officerTypes,
'reportData' => $reportData
]);
}
public function exportExcel() {
require_once APP_ROOT . '/app/Libraries/SimpleXLSXGen.php';
$payrollModel = new PayrollModel();
$hrModel = new HrPersonModel();
$officerTypeModel = new OfficerTypeModel();
$incomeModel = new IncomeMasterModel();
$deductionModel = new DeductionMasterModel();
$month = $_GET['month'] ?? date('m');
$year = $_GET['year'] ?? date('Y');
$typeId = $_GET['type_id'] ?? '';
$officerTypes = $officerTypeModel->getActive();
$selectedTypeName = 'ทั้งหมด';
if ($typeId) {
foreach ($officerTypes as $type) {
if ($type['id'] == $typeId) {
$selectedTypeName = $type['name'];
break;
}
}
}
$payrollData = $payrollModel->getSalariesByPeriod($month, $year);
$nationalIds = array_column($payrollData, 'national_id');
$hrData = $hrModel->getPersonsByNationalIds($nationalIds);
// Fetch all income/deduction codes to build dynamic columns
$allIncomes = $incomeModel->getAllIncomes();
$allDeductions = $deductionModel->getAllDeductions();
$incomeCols = [];
foreach ($allIncomes as $inc) {
$incomeCols[$inc['code']] = $inc['name'];
}
$deductionCols = [];
foreach ($allDeductions as $ded) {
$deductionCols[$ded['code']] = $ded['name'];
}
$reportData = [];
foreach ($payrollData as $row) {
$nid = $row['national_id'];
if (isset($hrData[$nid])) {
$personType = $hrData[$nid]['HR_PERSON_TYPE_NAME'] ?? '';
if ($typeId && $personType !== $selectedTypeName) {
continue;
}
$row['hr_prefix'] = $hrData[$nid]['HR_PREFIX_NAME'] ?? '';
$row['hr_fname'] = $hrData[$nid]['HR_FNAME'] ?? '';
$row['hr_lname'] = $hrData[$nid]['HR_LNAME'] ?? '';
// Fetch details
$incomes = $payrollModel->getEmployeeIncomes($row['id']);
$deductions = $payrollModel->getEmployeeDeductions($row['id']);
$row['incomes_detail'] = [];
foreach ($incomes as $inc) {
$row['incomes_detail'][$inc['income_code']] = $inc['amount'];
}
$row['deductions_detail'] = [];
foreach ($deductions as $ded) {
$row['deductions_detail'][$ded['deduction_code']] = $ded['amount'];
}
$reportData[] = $row;
}
}
usort($reportData, function($a, $b) {
return strcmp($a['hr_fname'], $b['hr_fname']);
});
$monthNames = [
'01' => 'มกราคม', '02' => 'กุมภาพันธ์', '03' => 'มีนาคม', '04' => 'เมษายน',
'05' => 'พฤษภาคม', '06' => 'มิถุนายน', '07' => 'กรกฎาคม', '08' => 'สิงหาคม',
'09' => 'กันยายน', '10' => 'ตุลาคม', '11' => 'พฤศจิกายน', '12' => 'ธันวาคม'
];
$monthName = $monthNames[$month] ?? $month;
$thaiYear = $year + 543;
$excelData = [];
// Header
$excelData[] = ["รายงานเงินเดือนเจ้าหน้าที่โรงพยาบาล"];
$excelData[] = ["ประจำเดือน: $monthName $thaiYear", "", "ประเภท: $selectedTypeName"];
$excelData[] = [];
// Columns
$headerRow = ["ลำดับ", "ชื่อ-สกุล", "เงินเดือนหลัก"];
foreach ($incomeCols as $code => $name) {
$headerRow[] = $name;
}
$headerRow[] = "รวมรายรับ";
foreach ($deductionCols as $code => $name) {
$headerRow[] = $name;
}
$headerRow[] = "รวมรายจ่าย";
$headerRow[] = "รับสุทธิ";
$excelData[] = $headerRow;
// Data Rows
$idx = 1;
foreach ($reportData as $row) {
$fullName = trim($row['hr_prefix']) . trim($row['hr_fname']) . ' ' . trim($row['hr_lname']);
$dataRow = [
$idx++,
$fullName,
$row['base_salary']
];
// Incomes
foreach ($incomeCols as $code => $name) {
$dataRow[] = $row['incomes_detail'][$code] ?? 0;
}
$dataRow[] = $row['total_income'];
// Deductions
foreach ($deductionCols as $code => $name) {
$dataRow[] = $row['deductions_detail'][$code] ?? 0;
}
$dataRow[] = $row['total_deduction'];
$dataRow[] = $row['net_salary'];
$excelData[] = $dataRow;
}
$xlsx = \Shuchkin\SimpleXLSXGen::fromArray($excelData);
$xlsx->freezePanes('A5');
$filename = "รายงานเงินเดือนเจ้าหน้าที่_{$month}_{$year}.xlsx";
$xlsx->downloadAs($filename);
exit;
}
}
@@ -0,0 +1,429 @@
<?php
namespace app\Controllers;
use app\Models\IncomeMasterModel;
use app\Models\DeductionMasterModel;
use app\Models\SystemSettingModel;
use app\Models\HrPersonModel;
use app\Models\OfficerTypeModel;
class SettingController extends Controller {
public function __construct() {
if (empty($_SESSION['user_id'])) {
$_SESSION['error'] = 'กรุณาเข้าสู่ระบบก่อนใช้งาน';
header('Location: ' . BASE_URL . '/login');
exit;
}
}
// --- Income Methods ---
public function income() {
$incomeModel = new IncomeMasterModel();
$incomes = $incomeModel->getAllIncomes();
$this->view('settings/income', [
'title' => 'จัดการรายการรายรับ | ' . APP_NAME,
'activeMenu' => 'setting_income',
'incomes' => $incomes
]);
}
public function storeIncome() {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$data = [
'code' => $_POST['code'] ?? '',
'name' => $_POST['name'] ?? '',
'description' => $_POST['description'] ?? '',
'is_taxable' => isset($_POST['is_taxable']) ? 1 : 0,
'is_active' => isset($_POST['is_active']) ? 1 : 0,
'fund_source' => $_POST['fund_source'] ?? 'HOSPITAL',
'display_order' => $_POST['display_order'] ?? 0
];
$incomeModel = new IncomeMasterModel();
try {
$incomeModel->createIncome($data);
$_SESSION['success'] = 'เพิ่มรายการรายรับสำเร็จ';
} catch (\PDOException $e) {
$_SESSION['error'] = 'ไม่สามารถเพิ่มรายการได้ อาจรหัสซ้ำ (' . $e->getMessage() . ')';
}
header('Location: ' . BASE_URL . '/settings/income');
exit;
}
}
public function updateIncome() {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = $_POST['id'] ?? null;
if (!$id) {
$_SESSION['error'] = 'ไม่พบรหัสรายการ';
header('Location: ' . BASE_URL . '/settings/income');
exit;
}
$data = [
'code' => $_POST['code'] ?? '',
'name' => $_POST['name'] ?? '',
'description' => $_POST['description'] ?? '',
'is_taxable' => isset($_POST['is_taxable']) ? 1 : 0,
'is_active' => isset($_POST['is_active']) ? 1 : 0,
'fund_source' => $_POST['fund_source'] ?? 'HOSPITAL',
'display_order' => $_POST['display_order'] ?? 0
];
$incomeModel = new IncomeMasterModel();
try {
$incomeModel->updateIncome($id, $data);
$_SESSION['success'] = 'แก้ไขรายการรายรับสำเร็จ';
} catch (\PDOException $e) {
$_SESSION['error'] = 'ไม่สามารถแก้ไขรายการได้ (' . $e->getMessage() . ')';
}
header('Location: ' . BASE_URL . '/settings/income');
exit;
}
}
// --- Deduction Methods ---
public function deduction() {
$deductionModel = new DeductionMasterModel();
$deductions = $deductionModel->getAllDeductions();
$this->view('settings/deduction', [
'title' => 'จัดการรายการรายจ่าย | ' . APP_NAME,
'activeMenu' => 'setting_deduction',
'deductions' => $deductions
]);
}
public function storeDeduction() {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$data = [
'code' => $_POST['code'] ?? '',
'name' => $_POST['name'] ?? '',
'description' => $_POST['description'] ?? '',
'is_tax_deductible' => isset($_POST['is_tax_deductible']) ? 1 : 0,
'is_active' => isset($_POST['is_active']) ? 1 : 0,
'fund_source' => $_POST['fund_source'] ?? 'HOSPITAL',
'display_order' => $_POST['display_order'] ?? 0
];
$deductionModel = new DeductionMasterModel();
try {
$deductionModel->createDeduction($data);
$_SESSION['success'] = 'เพิ่มรายการรายจ่ายสำเร็จ';
} catch (\PDOException $e) {
$_SESSION['error'] = 'ไม่สามารถเพิ่มรายการได้ อาจรหัสซ้ำ (' . $e->getMessage() . ')';
}
header('Location: ' . BASE_URL . '/settings/deduction');
exit;
}
}
public function updateDeduction() {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = $_POST['id'] ?? null;
if (!$id) {
$_SESSION['error'] = 'ไม่พบรหัสรายการ';
header('Location: ' . BASE_URL . '/settings/deduction');
exit;
}
$data = [
'code' => $_POST['code'] ?? '',
'name' => $_POST['name'] ?? '',
'description' => $_POST['description'] ?? '',
'is_tax_deductible' => isset($_POST['is_tax_deductible']) ? 1 : 0,
'is_active' => isset($_POST['is_active']) ? 1 : 0,
'fund_source' => $_POST['fund_source'] ?? 'HOSPITAL',
'display_order' => $_POST['display_order'] ?? 0
];
$deductionModel = new DeductionMasterModel();
try {
$deductionModel->updateDeduction($id, $data);
$_SESSION['success'] = 'แก้ไขรายการรายจ่ายสำเร็จ';
} catch (\PDOException $e) {
$_SESSION['error'] = 'ไม่สามารถแก้ไขรายการได้ (' . $e->getMessage() . ')';
}
header('Location: ' . BASE_URL . '/settings/deduction');
exit;
}
}
// --- System Methods ---
public function system() {
$settingModel = new SystemSettingModel();
$orgTaxId = $settingModel->getSetting('org_tax_id', '');
$appVersion = $settingModel->getSetting('app_version', '1.0.0');
$signatureName = $settingModel->getSetting('signature_name', '');
$signaturePosition = $settingModel->getSetting('signature_position', '');
$signatureScale = $settingModel->getSetting('signature_scale', '100');
$slipFontSize = $settingModel->getSetting('slip_font_size', '11pt');
$signaturePosX = $settingModel->getSetting('signature_pos_x', '0');
$signaturePosY = $settingModel->getSetting('signature_pos_y', '0');
$chartHistoryYears = $settingModel->getSetting('chart_history_years', '3');
$this->view('settings/system', [
'title' => 'ตั้งค่าระบบทั่วไป | ' . APP_NAME,
'activeMenu' => 'setting_system',
'orgTaxId' => $orgTaxId,
'appVersion' => $appVersion,
'signatureName' => $signatureName,
'signaturePosition' => $signaturePosition,
'signatureScale' => $signatureScale,
'slipFontSize' => $slipFontSize,
'signaturePosX' => $signaturePosX,
'signaturePosY' => $signaturePosY,
'chartHistoryYears' => $chartHistoryYears,
'taxSignatureName' => $settingModel->getSetting('tax_signature_name', ''),
'taxSignaturePosition' => $settingModel->getSetting('tax_signature_position', ''),
'taxSignatureScale' => $settingModel->getSetting('tax_signature_scale', '100'),
'taxSignaturePosX' => $settingModel->getSetting('tax_signature_pos_x', '0'),
'taxSignaturePosY' => $settingModel->getSetting('tax_signature_pos_y', '0'),
'taxFontSize' => $settingModel->getSetting('tax_font_size', '11pt'),
'orgAddress' => $settingModel->getSetting('org_address', 'ต.อ่างทอง อ.เกาะสมุย จ.สุราษฎร์ธานี 84140'),
'taxPayeeAddress' => $settingModel->getSetting('tax_payee_address', 'ต.อ่างทอง อ.เกาะสมุย จ.สุราษฎร์ธานี 84140')
]);
}
public function saveSystemSettings() {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$settingModel = new SystemSettingModel();
$settingModel->setSetting('org_tax_id', $_POST['org_tax_id'] ?? '');
$settingModel->setSetting('app_version', $_POST['app_version'] ?? '1.0.0');
$settingModel->setSetting('signature_name', $_POST['signature_name'] ?? '');
$settingModel->setSetting('signature_position', $_POST['signature_position'] ?? '');
$settingModel->setSetting('signature_scale', $_POST['signature_scale'] ?? '100');
$settingModel->setSetting('slip_font_size', $_POST['slip_font_size'] ?? '11pt');
$settingModel->setSetting('signature_pos_x', $_POST['signature_pos_x'] ?? '0');
$settingModel->setSetting('signature_pos_y', $_POST['signature_pos_y'] ?? '0');
$settingModel->setSetting('chart_history_years', $_POST['chart_history_years'] ?? '3');
$settingModel->setSetting('tax_signature_name', $_POST['tax_signature_name'] ?? '');
$settingModel->setSetting('tax_signature_position', $_POST['tax_signature_position'] ?? '');
$settingModel->setSetting('tax_signature_scale', $_POST['tax_signature_scale'] ?? '100');
$settingModel->setSetting('tax_signature_pos_x', $_POST['tax_signature_pos_x'] ?? '0');
$settingModel->setSetting('tax_signature_pos_y', $_POST['tax_signature_pos_y'] ?? '0');
$settingModel->setSetting('tax_font_size', $_POST['tax_font_size'] ?? '11pt');
$settingModel->setSetting('org_address', $_POST['org_address'] ?? 'ต.อ่างทอง อ.เกาะสมุย จ.สุราษฎร์ธานี 84140');
$settingModel->setSetting('tax_payee_address', $_POST['tax_payee_address'] ?? 'ต.อ่างทอง อ.เกาะสมุย จ.สุราษฎร์ธานี 84140');
$_SESSION['success'] = 'บันทึกการตั้งค่าระบบเรียบร้อยแล้ว';
header('Location: ' . BASE_URL . '/settings/system');
exit;
}
}
public function searchOfficer() {
error_reporting(0); // Supress all errors to prevent breaking JSON
header('Content-Type: application/json; charset=utf-8');
$keyword = $_GET['q'] ?? '';
$response = [
'status' => 'error',
'message' => 'ไม่พบข้อมูล',
'data' => []
];
if (strlen($keyword) >= 2) {
try {
$hrModel = new HrPersonModel();
$results = $hrModel->searchPersons($keyword);
if (!empty($results)) {
$formattedData = [];
foreach ($results as $person) {
// User's formatting logic
$fullName = ($person['HR_PREFIX_NAME'] ?? '') . ($person['HR_FNAME'] ?? '') . ' ' . ($person['HR_LNAME'] ?? '');
$department = $person['HR_DEPARTMENT_SUB_SUB_NAME'] ?? 'ไม่ระบุ';
$position = ($person['HR_POSITION_NAME'] ?? '') . ' ' . ($person['HR_LEVEL_NAME'] ?? '');
$formattedData[] = [
'id' => $person['HR_CID'] ?? '',
'text' => trim($fullName) . ' (' . trim($position) . ')',
'name' => trim($fullName),
'position' => trim($position)
];
}
$response = [
'status' => 'success',
'message' => 'พบข้อมูล',
'data' => $formattedData
];
}
} catch (\Exception $e) {
$response['message'] = $e->getMessage();
}
}
echo json_encode($response);
exit;
}
public function uploadSignature() {
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['signature'])) {
$file = $_FILES['signature'];
// Check for errors
if ($file['error'] !== UPLOAD_ERR_OK) {
$_SESSION['error'] = 'เกิดข้อผิดพลาดในการอัปโหลดไฟล์ (Error Code: ' . $file['error'] . ')';
header('Location: ' . BASE_URL . '/settings/system');
exit;
}
// Validate MIME type
$allowedTypes = ['image/jpeg', 'image/png'];
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($mimeType, $allowedTypes)) {
$_SESSION['error'] = 'กรุณาอัปโหลดไฟล์รูปภาพ JPG หรือ PNG เท่านั้น';
header('Location: ' . BASE_URL . '/settings/system');
exit;
}
// Save as signature.png
$targetPath = APP_ROOT . '/public/img/signature.png';
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
$_SESSION['success'] = 'อัปโหลดรูปลายเซ็นสำเร็จ';
} else {
$_SESSION['error'] = 'ไม่สามารถบันทึกไฟล์ได้';
}
header('Location: ' . BASE_URL . '/settings/system');
exit;
}
}
public function uploadTaxSignature() {
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['tax_signature'])) {
$file = $_FILES['tax_signature'];
// Check for errors
if ($file['error'] !== UPLOAD_ERR_OK) {
$_SESSION['error'] = 'เกิดข้อผิดพลาดในการอัปโหลดไฟล์ (Error Code: ' . $file['error'] . ')';
header('Location: ' . BASE_URL . '/settings/system');
exit;
}
// Validate MIME type
$allowedTypes = ['image/jpeg', 'image/png'];
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($mimeType, $allowedTypes)) {
$_SESSION['error'] = 'กรุณาอัปโหลดไฟล์รูปภาพ JPG หรือ PNG เท่านั้น';
header('Location: ' . BASE_URL . '/settings/system');
exit;
}
// Save as tax_signature.png
$targetPath = APP_ROOT . '/public/img/tax_signature.png';
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
$_SESSION['success'] = 'อัปโหลดรูปลายเซ็นสำหรับใบเสียภาษีสำเร็จ';
} else {
$_SESSION['error'] = 'ไม่สามารถบันทึกไฟล์ได้';
}
header('Location: ' . BASE_URL . '/settings/system');
exit;
}
}
// --- Officer Types ---
public function officerTypes() {
$model = new OfficerTypeModel();
$officerTypes = $model->getAll();
$this->view('settings/officer_types', [
'title' => 'จัดการประเภทเจ้าหน้าที่ | ' . APP_NAME,
'activeMenu' => 'setting_officer_types',
'officerTypes' => $officerTypes
]);
}
public function storeOfficerType() {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$name = trim($_POST['name'] ?? '');
$isActive = isset($_POST['is_active']) ? 1 : 0;
if (empty($name)) {
$_SESSION['error'] = 'กรุณากรอกชื่อประเภทเจ้าหน้าที่';
header('Location: ' . BASE_URL . '/settings/officer-types');
exit;
}
$model = new OfficerTypeModel();
if ($model->create($name, $isActive)) {
$_SESSION['success'] = 'เพิ่มประเภทเจ้าหน้าที่สำเร็จ';
} else {
$_SESSION['error'] = 'ไม่สามารถเพิ่มข้อมูลได้ (ชื่ออาจซ้ำ)';
}
header('Location: ' . BASE_URL . '/settings/officer-types');
exit;
}
}
public function updateOfficerType() {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = $_POST['id'] ?? null;
$name = trim($_POST['name'] ?? '');
$isActive = isset($_POST['is_active']) ? 1 : 0;
if (!$id || empty($name)) {
$_SESSION['error'] = 'ข้อมูลไม่ครบถ้วน';
header('Location: ' . BASE_URL . '/settings/officer-types');
exit;
}
$model = new OfficerTypeModel();
if ($model->update($id, $name, $isActive)) {
$_SESSION['success'] = 'แก้ไขประเภทเจ้าหน้าที่สำเร็จ';
} else {
$_SESSION['error'] = 'ไม่สามารถแก้ไขข้อมูลได้';
}
header('Location: ' . BASE_URL . '/settings/officer-types');
exit;
}
}
public function toggleOfficerTypeStatus() {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = $_POST['id'] ?? null;
$status = isset($_POST['status']) && $_POST['status'] == '1' ? 1 : 0;
if ($id) {
$model = new OfficerTypeModel();
$model->toggleActive($id, $status);
}
header('Location: ' . BASE_URL . '/settings/officer-types');
exit;
}
}
public function deleteOfficerType() {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = $_POST['id'] ?? null;
if ($id) {
$model = new OfficerTypeModel();
$model->delete($id);
$_SESSION['success'] = 'ลบประเภทเจ้าหน้าที่สำเร็จ';
}
header('Location: ' . BASE_URL . '/settings/officer-types');
exit;
}
}
}
@@ -0,0 +1,139 @@
<?php
namespace app\Controllers;
use app\Models\UserModel;
use app\Models\UserLogModel;
class UserController extends Controller {
public function __construct() {
if (empty($_SESSION['user_id'])) {
$_SESSION['error'] = 'กรุณาเข้าสู่ระบบก่อนใช้งาน';
header('Location: ' . BASE_URL . '/login');
exit;
}
// Basic Role Check (Only Admin can manage users)
if ($_SESSION['role_id'] != 1) {
$_SESSION['error'] = 'คุณไม่มีสิทธิ์เข้าถึงหน้านี้';
header('Location: ' . BASE_URL . '/dashboard');
exit;
}
}
public function index() {
$userModel = new UserModel();
$users = $userModel->getAllUsers();
$roles = $userModel->getRoles();
$this->view('users/index', [
'title' => 'จัดการผู้ใช้งานระบบ | ' . APP_NAME,
'activeMenu' => 'users',
'users' => $users,
'roles' => $roles
]);
}
public function logs() {
$userModel = new UserModel();
$userLogModel = new UserLogModel();
$filters = [
'user_id' => $_GET['user_id'] ?? '',
'action' => $_GET['action'] ?? '',
'date_start' => $_GET['date_start'] ?? '',
'date_end' => $_GET['date_end'] ?? ''
];
$users = $userModel->getAllUsers();
$logs = $userLogModel->getLogs($filters);
$this->view('users/logs', [
'title' => 'ประวัติการใช้งานระบบ | ' . APP_NAME,
'activeMenu' => 'users',
'users' => $users,
'logs' => $logs,
'filters' => $filters
]);
}
public function store() {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$data = [
'username' => $_POST['username'] ?? '',
'password' => $_POST['password'] ?? '',
'role_id' => $_POST['role_id'] ?? 2,
'first_name' => $_POST['first_name'] ?? '',
'last_name' => $_POST['last_name'] ?? '',
'status' => isset($_POST['status']) ? 'active' : 'inactive'
];
$userModel = new UserModel();
try {
$userModel->createUser($data);
$logModel = new UserLogModel();
$logModel->logAction($_SESSION['user_id'], 'CREATE_USER', 'เพิ่มผู้ใช้งานใหม่ Username: ' . $data['username']);
$_SESSION['success'] = 'เพิ่มผู้ใช้งานสำเร็จ';
} catch (\PDOException $e) {
$_SESSION['error'] = 'ไม่สามารถเพิ่มผู้ใช้งานได้ (Username อาจซ้ำ)';
}
header('Location: ' . BASE_URL . '/users');
exit;
}
}
public function update() {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$data = [
'id' => $_POST['id'] ?? '',
'first_name' => $_POST['first_name'] ?? '',
'last_name' => $_POST['last_name'] ?? '',
'role_id' => $_POST['role_id'] ?? 2,
'password' => $_POST['password'] ?? ''
];
$userModel = new UserModel();
try {
$userModel->updateUser($data);
$logModel = new UserLogModel();
$logModel->logAction($_SESSION['user_id'], 'UPDATE_USER', 'แก้ไขข้อมูลผู้ใช้งาน ID: ' . $data['id']);
$_SESSION['success'] = 'แก้ไขข้อมูลผู้ใช้งานสำเร็จ';
} catch (\PDOException $e) {
$_SESSION['error'] = 'ไม่สามารถแก้ไขข้อมูลได้';
}
header('Location: ' . BASE_URL . '/users');
exit;
}
}
public function toggle_status() {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$userId = $_POST['user_id'] ?? '';
$status = $_POST['status'] ?? 'inactive';
// Prevent locking out the only admin or self if needed (optional)
if ($userId == $_SESSION['user_id'] && $status == 'inactive') {
$_SESSION['error'] = 'ไม่สามารถปิดสิทธิ์บัญชีตัวเองได้';
} else {
$userModel = new UserModel();
try {
$userModel->updateStatus($userId, $status);
$logModel = new UserLogModel();
$logModel->logAction($_SESSION['user_id'], 'TOGGLE_STATUS', "เปลี่ยนสถานะผู้ใช้งาน ID: $userId เป็น $status");
$_SESSION['success'] = 'ปรับสถานะการใช้งานสำเร็จ';
} catch (\PDOException $e) {
$_SESSION['error'] = 'ไม่สามารถปรับสถานะได้';
}
}
header('Location: ' . BASE_URL . '/users');
exit;
}
}
}
@@ -0,0 +1,67 @@
<?php
namespace app\Models;
use PDO;
class DeductionMasterModel extends Model {
public function getAllDeductions() {
$stmt = $this->db->prepare("SELECT * FROM deduction_master ORDER BY fund_source ASC, display_order ASC");
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
public function getActiveDeductions() {
$stmt = $this->db->prepare("SELECT * FROM deduction_master WHERE is_active = 1 ORDER BY display_order ASC, id ASC");
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
public function getDeductionById($id) {
$stmt = $this->db->prepare("SELECT * FROM deduction_master WHERE id = ?");
$stmt->execute([$id]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
public function createDeduction($data) {
$sql = "INSERT INTO deduction_master (code, name, description, is_tax_deductible, is_active, display_order, fund_source)
VALUES (?, ?, ?, ?, ?, ?, ?)";
$stmt = $this->db->prepare($sql);
return $stmt->execute([
$data['code'],
$data['name'],
$data['description'] ?? null,
$data['is_tax_deductible'] ?? 0,
$data['is_active'] ?? 1,
$data['display_order'] ?? 0,
$data['fund_source'] ?? 'HOSPITAL'
]);
}
public function updateDeduction($id, $data) {
$sql = "UPDATE deduction_master
SET code = ?, name = ?, description = ?, is_tax_deductible = ?, is_active = ?, display_order = ?, fund_source = ?
WHERE id = ?";
$stmt = $this->db->prepare($sql);
return $stmt->execute([
$data['code'],
$data['name'],
$data['description'] ?? null,
$data['is_tax_deductible'] ?? 0,
$data['is_active'] ?? 1,
$data['display_order'] ?? 0,
$data['fund_source'] ?? 'HOSPITAL',
$id
]);
}
public function deleteDeduction($id) {
$stmt = $this->db->prepare("DELETE FROM deduction_master WHERE id = ?");
return $stmt->execute([$id]);
}
public function addDeduction($code, $name, $fundSource = 'HOSPITAL', $displayOrder = 0) {
$stmt = $this->db->prepare("INSERT INTO deduction_master (code, name, fund_source, display_order) VALUES (?, ?, ?, ?)");
return $stmt->execute([$code, $name, $fundSource, $displayOrder]);
}
}
@@ -0,0 +1,21 @@
<?php
namespace app\Models;
use PDO;
class EmployeeProfileModel extends Model {
public function getProfile($nationalId) {
$stmt = $this->db->prepare("SELECT * FROM employee_profiles WHERE national_id = ?");
$stmt->execute([$nationalId]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
public function saveProfile($nationalId, $bankName, $bankAccount) {
$sql = "INSERT INTO employee_profiles (national_id, bank_name, bank_account)
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE bank_name = VALUES(bank_name), bank_account = VALUES(bank_account)";
$stmt = $this->db->prepare($sql);
return $stmt->execute([$nationalId, $bankName, $bankAccount]);
}
}
@@ -0,0 +1,130 @@
<?php
namespace app\Models;
use Database;
use PDO;
class HrPersonModel {
protected $db;
public function __construct() {
// Use the external HR database connection
$this->db = Database::getHrConnection();
}
/**
* Get person details by National ID (HR_CID)
*/
public function getPersonByNationalId($nationalId) {
$sql = "SELECT a.HR_CID, a.HR_FNAME, a.HR_LNAME, a.HR_POSITION_NUM,
b.HR_DEPARTMENT_SUB_SUB_NAME,
c.HR_LEVEL_NAME,
d.HR_PERSON_TYPE_NAME,
e.HR_PREFIX_NAME,
f.HR_POSITION_NAME
FROM hr_person a
LEFT OUTER JOIN hr_department_sub_sub b on a.HR_DEPARTMENT_SUB_SUB_ID = b.HR_DEPARTMENT_SUB_SUB_ID
LEFT OUTER JOIN hr_level c ON a.HR_LEVEL_ID = c.HR_LEVEL_ID
LEFT OUTER JOIN hr_person_type d ON a.HR_PERSON_TYPE_ID = d.HR_PERSON_TYPE_ID
LEFT OUTER JOIN hr_prefix e ON a.HR_PREFIX_ID=e.HR_PREFIX_ID
LEFT OUTER JOIN hr_position f ON a.HR_POSITION_ID=f.HR_POSITION_ID
WHERE REPLACE(a.HR_CID, '-', '') = :cid LIMIT 1";
try {
$stmt = $this->db->prepare($sql);
$stmt->execute(['cid' => $nationalId]);
return $stmt->fetch(PDO::FETCH_ASSOC);
} catch (\PDOException $e) {
error_log("Error fetching HR person: " . $e->getMessage());
return false;
}
}
/**
* Get person image by National ID (HR_CID)
*/
public function getPersonImage($nationalId) {
$sql = "SELECT HR_IMAGE FROM hr_person WHERE REPLACE(HR_CID, '-', '') = :cid LIMIT 1";
try {
$stmt = $this->db->prepare($sql);
$stmt->execute(['cid' => $nationalId]);
return $stmt->fetch(PDO::FETCH_ASSOC);
} catch (\PDOException $e) {
error_log("Error fetching HR person image: " . $e->getMessage());
return false;
}
}
/**
* Get multiple persons by an array of National IDs
*/
public function getPersonsByNationalIds(array $cids) {
if (empty($cids)) {
return [];
}
// Create placeholders ?, ?, ?
$placeholders = implode(',', array_fill(0, count($cids), '?'));
$sql = "SELECT a.HR_CID, a.HR_FNAME, a.HR_LNAME, a.HR_POSITION_NUM,
b.HR_DEPARTMENT_SUB_SUB_NAME,
c.HR_LEVEL_NAME,
d.HR_PERSON_TYPE_NAME,
e.HR_PREFIX_NAME,
f.HR_POSITION_NAME
FROM hr_person a
LEFT OUTER JOIN hr_department_sub_sub b on a.HR_DEPARTMENT_SUB_SUB_ID = b.HR_DEPARTMENT_SUB_SUB_ID
LEFT OUTER JOIN hr_level c ON a.HR_LEVEL_ID = c.HR_LEVEL_ID
LEFT OUTER JOIN hr_person_type d ON a.HR_PERSON_TYPE_ID = d.HR_PERSON_TYPE_ID
LEFT OUTER JOIN hr_prefix e ON a.HR_PREFIX_ID=e.HR_PREFIX_ID
LEFT OUTER JOIN hr_position f ON a.HR_POSITION_ID=f.HR_POSITION_ID
WHERE a.HR_CID IN ($placeholders)";
try {
$stmt = $this->db->prepare($sql);
$stmt->execute($cids);
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
$mapped = [];
foreach ($results as $row) {
$mapped[$row['HR_CID']] = $row;
}
return $mapped;
} catch (\PDOException $e) {
error_log("Error fetching multiple HR persons: " . $e->getMessage());
return [];
}
}
/**
* Search persons by keyword
*/
public function searchPersons($keyword) {
if (empty($keyword)) {
return [];
}
$sql = "SELECT * FROM hr_person a
LEFT OUTER JOIN hr_department_sub_sub b on a.HR_DEPARTMENT_SUB_SUB_ID = b.HR_DEPARTMENT_SUB_SUB_ID
LEFT OUTER JOIN hr_level c ON a.HR_LEVEL_ID = c.HR_LEVEL_ID
LEFT OUTER JOIN hr_person_type d ON a.HR_PERSON_TYPE_ID = d.HR_PERSON_TYPE_ID
LEFT OUTER JOIN hr_prefix e ON a.HR_PREFIX_ID=e.HR_PREFIX_ID
LEFT OUTER JOIN hr_position f ON a.HR_POSITION_ID=f.HR_POSITION_ID
WHERE (REPLACE(a.HR_CID, '-', '') = ? OR a.HR_FNAME LIKE ? OR a.HR_LNAME LIKE ?)
AND a.HR_STATUS_ID='01'
LIMIT 15";
try {
$kw = '%' . $keyword . '%';
$stmt = $this->db->prepare($sql);
$stmt->execute([$keyword, $kw, $kw]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (\Exception $e) {
error_log("Error searching HR persons: " . $e->getMessage());
return [];
}
}
}
?>
@@ -0,0 +1,67 @@
<?php
namespace app\Models;
use PDO;
class IncomeMasterModel extends Model {
public function getAllIncomes() {
$stmt = $this->db->prepare("SELECT * FROM income_master ORDER BY fund_source ASC, display_order ASC");
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
public function getActiveIncomes() {
$stmt = $this->db->prepare("SELECT * FROM income_master WHERE is_active = 1 ORDER BY display_order ASC, id ASC");
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
public function getIncomeById($id) {
$stmt = $this->db->prepare("SELECT * FROM income_master WHERE id = ?");
$stmt->execute([$id]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
public function createIncome($data) {
$sql = "INSERT INTO income_master (code, name, description, is_taxable, is_active, display_order, fund_source)
VALUES (?, ?, ?, ?, ?, ?, ?)";
$stmt = $this->db->prepare($sql);
return $stmt->execute([
$data['code'],
$data['name'],
$data['description'] ?? null,
$data['is_taxable'] ?? 1,
$data['is_active'] ?? 1,
$data['display_order'] ?? 0,
$data['fund_source'] ?? 'HOSPITAL'
]);
}
public function updateIncome($id, $data) {
$sql = "UPDATE income_master
SET code = ?, name = ?, description = ?, is_taxable = ?, is_active = ?, display_order = ?, fund_source = ?
WHERE id = ?";
$stmt = $this->db->prepare($sql);
return $stmt->execute([
$data['code'],
$data['name'],
$data['description'] ?? null,
$data['is_taxable'] ?? 1,
$data['is_active'] ?? 1,
$data['display_order'] ?? 0,
$data['fund_source'] ?? 'HOSPITAL',
$id
]);
}
public function deleteIncome($id) {
$stmt = $this->db->prepare("DELETE FROM income_master WHERE id = ?");
return $stmt->execute([$id]);
}
public function addIncome($code, $name, $isTaxable, $fundSource = 'HOSPITAL', $displayOrder = 0) {
$stmt = $this->db->prepare("INSERT INTO income_master (code, name, is_taxable, fund_source, display_order) VALUES (?, ?, ?, ?, ?)");
return $stmt->execute([$code, $name, $isTaxable, $fundSource, $displayOrder]);
}
}
@@ -0,0 +1,14 @@
<?php
namespace app\Models;
use Database;
use PDO;
abstract class Model {
protected $db;
public function __construct() {
$this->db = Database::getInstance();
}
}
?>
@@ -0,0 +1,82 @@
<?php
namespace app\Models;
use PDO;
use Database;
class OfficerTypeModel extends Model {
public function __construct() {
parent::__construct();
$this->initTable();
}
private function initTable() {
$sql = "CREATE TABLE IF NOT EXISTS officer_types (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL UNIQUE,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)";
$this->db->exec($sql);
// Check if empty and seed
$stmt = $this->db->query("SELECT COUNT(*) FROM officer_types");
if ($stmt->fetchColumn() == 0) {
$seed = "INSERT IGNORE INTO officer_types (name, is_active) VALUES
('ข้าราชการ', 1),
('ลูกจ้างประจำ', 1),
('พนักงานราชการ', 1),
('พนักงานกระทรวงสาธารณสุข', 1),
('ลูกจ้างชั่วคราว', 1),
('ลูกจ้างรายวัน', 1)";
$this->db->exec($seed);
}
}
public function getAll() {
$stmt = $this->db->query("SELECT * FROM officer_types ORDER BY id ASC");
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
public function getActive() {
$stmt = $this->db->query("SELECT * FROM officer_types WHERE is_active = 1 ORDER BY id ASC");
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
public function getById($id) {
$stmt = $this->db->prepare("SELECT * FROM officer_types WHERE id = :id");
$stmt->execute(['id' => $id]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
public function create($name, $is_active = 1) {
try {
$stmt = $this->db->prepare("INSERT INTO officer_types (name, is_active) VALUES (:name, :is_active)");
return $stmt->execute(['name' => $name, 'is_active' => $is_active]);
} catch (\PDOException $e) {
error_log("Error creating officer type: " . $e->getMessage());
return false;
}
}
public function update($id, $name, $is_active) {
try {
$stmt = $this->db->prepare("UPDATE officer_types SET name = :name, is_active = :is_active WHERE id = :id");
return $stmt->execute(['id' => $id, 'name' => $name, 'is_active' => $is_active]);
} catch (\PDOException $e) {
error_log("Error updating officer type: " . $e->getMessage());
return false;
}
}
public function toggleActive($id, $status) {
$stmt = $this->db->prepare("UPDATE officer_types SET is_active = :status WHERE id = :id");
return $stmt->execute(['id' => $id, 'status' => $status]);
}
public function delete($id) {
$stmt = $this->db->prepare("DELETE FROM officer_types WHERE id = :id");
return $stmt->execute(['id' => $id]);
}
}
@@ -0,0 +1,560 @@
<?php
namespace app\Models;
use PDO;
class PayrollModel extends Model {
/**
* Get all payroll months with their years
*/
public function getAllPayrollPeriods() {
$sql = "
SELECT sm.*, sy.year_no
FROM salary_month sm
JOIN salary_year sy ON sm.year_id = sy.id
ORDER BY sy.year_no DESC, sm.month_no DESC
";
$stmt = $this->db->prepare($sql);
$stmt->execute();
return $stmt->fetchAll();
}
/**
* Check if a specific month and year already exists
*/
public function checkPeriodExists($yearNo, $monthNo) {
$sql = "
SELECT sm.id
FROM salary_month sm
JOIN salary_year sy ON sm.year_id = sy.id
WHERE sy.year_no = :yearNo AND sm.month_no = :monthNo
LIMIT 1
";
$stmt = $this->db->prepare($sql);
$stmt->bindParam(':yearNo', $yearNo, PDO::PARAM_INT);
$stmt->bindParam(':monthNo', $monthNo, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetch() !== false;
}
/**
* Create a new payroll period
*/
public function createPayrollPeriod($yearNo, $monthNo) {
try {
$this->db->beginTransaction();
// 1. Ensure Year Exists
$stmtYear = $this->db->prepare("SELECT id FROM salary_year WHERE year_no = :yearNo LIMIT 1");
$stmtYear->bindParam(':yearNo', $yearNo, PDO::PARAM_INT);
$stmtYear->execute();
$yearRow = $stmtYear->fetch();
if ($yearRow) {
$yearId = $yearRow['id'];
} else {
$stmtInsertYear = $this->db->prepare("INSERT INTO salary_year (year_no) VALUES (:yearNo)");
$stmtInsertYear->bindParam(':yearNo', $yearNo, PDO::PARAM_INT);
$stmtInsertYear->execute();
$yearId = $this->db->lastInsertId();
}
// 2. Create Month (Draft)
$stmtInsertMonth = $this->db->prepare("INSERT INTO salary_month (year_id, month_no, status) VALUES (:yearId, :monthNo, 'Draft')");
$stmtInsertMonth->bindParam(':yearId', $yearId, PDO::PARAM_INT);
$stmtInsertMonth->bindParam(':monthNo', $monthNo, PDO::PARAM_INT);
$stmtInsertMonth->execute();
$this->db->commit();
return true;
} catch (\PDOException $e) {
error_log("Error creating payroll period: " . $e->getMessage());
return false;
}
}
/**
* Get the previous salary month ID relative to the given year and month
*/
public function getPreviousSalaryMonth($yearNo, $monthNo) {
$prevMonth = $monthNo - 1;
$prevYear = $yearNo;
if ($prevMonth < 1) {
$prevMonth = 12;
$prevYear--;
}
$sql = "SELECT m.id
FROM salary_month m
JOIN salary_year y ON m.year_id = y.id
WHERE y.year_no = :yearNo AND m.month_no = :monthNo";
try {
$stmt = $this->db->prepare($sql);
$stmt->execute(['yearNo' => $prevYear, 'monthNo' => $prevMonth]);
$result = $stmt->fetch();
return $result ? $result['id'] : false;
} catch (\PDOException $e) {
error_log("Error getting previous salary month: " . $e->getMessage());
return false;
}
}
/**
* Copy employees and their recurring items from previous month
*/
public function copyFromPreviousMonth($currentMonthId, $previousMonthId) {
try {
$this->beginTransaction();
// 1. Get ALL employees from previous month
$sqlEmployees = "SELECT * FROM employee_salary WHERE salary_month_id = :prev_id";
$stmtEmp = $this->db->prepare($sqlEmployees);
$stmtEmp->execute(['prev_id' => $previousMonthId]);
$prevEmployees = $stmtEmp->fetchAll();
$copiedCount = 0;
$mergedCount = 0;
// Prepare statements for inserting NEW employee
$stmtInsertEmp = $this->db->prepare("
INSERT INTO employee_salary (
salary_month_id, employee_code, national_id, first_name, last_name,
position, department, base_salary, total_income, total_deduction, net_salary
) VALUES (
:salary_month_id, :employee_code, :national_id, :first_name, :last_name,
:position, :department, :base_salary, :total_income, :total_deduction, :net_salary
)
");
// Prepare statements for getting items from previous month
$stmtGetIncomes = $this->db->prepare("
SELECT si.income_master_id, si.amount, im.fund_source
FROM salary_income si
JOIN income_master im ON si.income_master_id = im.id
WHERE si.employee_salary_id = ?
");
$stmtGetDeductions = $this->db->prepare("
SELECT sd.deduction_master_id, sd.amount, dm.fund_source
FROM salary_deduction sd
JOIN deduction_master dm ON sd.deduction_master_id = dm.id
WHERE sd.employee_salary_id = ?
");
// Prepare statements for inserting items
$stmtInsertIncome = $this->db->prepare("INSERT INTO salary_income (employee_salary_id, income_master_id, amount) VALUES (?, ?, ?)");
$stmtInsertDeduction = $this->db->prepare("INSERT INTO salary_deduction (employee_salary_id, deduction_master_id, amount) VALUES (?, ?, ?)");
// Prepare statements for checking existing items (to prevent duplicates)
$stmtCheckIncome = $this->db->prepare("SELECT 1 FROM salary_income WHERE employee_salary_id = ? AND income_master_id = ?");
$stmtCheckDeduction = $this->db->prepare("SELECT 1 FROM salary_deduction WHERE employee_salary_id = ? AND deduction_master_id = ?");
// Prepare statement for updating totals of existing employee
$stmtUpdateTotals = $this->db->prepare("
UPDATE employee_salary
SET total_income = total_income + ?,
total_deduction = total_deduction + ?,
net_salary = net_salary + ? - ?
WHERE id = ?
");
// Prepare statement to check if employee already exists in current month
$stmtCheckEmp = $this->db->prepare("SELECT id FROM employee_salary WHERE salary_month_id = ? AND national_id = ?");
foreach ($prevEmployees as $emp) {
// Check if employee already exists in current month
$stmtCheckEmp->execute([$currentMonthId, $emp['national_id']]);
$existingEmp = $stmtCheckEmp->fetch();
if (!$existingEmp) {
// --- CASE 1: Employee DOES NOT exist in current month (Fully copy) ---
$stmtInsertEmp->execute([
'salary_month_id' => $currentMonthId,
'employee_code' => $emp['employee_code'],
'national_id' => $emp['national_id'],
'first_name' => $emp['first_name'],
'last_name' => $emp['last_name'],
'position' => $emp['position'],
'department' => $emp['department'],
'base_salary' => $emp['base_salary'],
'total_income' => $emp['total_income'],
'total_deduction' => $emp['total_deduction'],
'net_salary' => $emp['net_salary']
]);
$newEmpId = $this->db->lastInsertId();
// Copy ALL incomes
$stmtGetIncomes->execute([$emp['id']]);
$incomes = $stmtGetIncomes->fetchAll();
foreach ($incomes as $inc) {
$stmtInsertIncome->execute([$newEmpId, $inc['income_master_id'], $inc['amount']]);
}
// Copy ALL deductions
$stmtGetDeductions->execute([$emp['id']]);
$deductions = $stmtGetDeductions->fetchAll();
foreach ($deductions as $ded) {
$stmtInsertDeduction->execute([$newEmpId, $ded['deduction_master_id'], $ded['amount']]);
}
$copiedCount++;
} else {
// --- CASE 2: Employee EXISTS in current month (Merge HOSPITAL items) ---
$currEmpId = $existingEmp['id'];
$addedIncomeAmount = 0;
$addedDeductionAmount = 0;
// Copy ONLY HOSPITAL incomes
$stmtGetIncomes->execute([$emp['id']]);
$incomes = $stmtGetIncomes->fetchAll();
foreach ($incomes as $inc) {
if (strtoupper($inc['fund_source']) === 'HOSPITAL') {
// Prevent duplicate insertion
$stmtCheckIncome->execute([$currEmpId, $inc['income_master_id']]);
if (!$stmtCheckIncome->fetch()) {
$stmtInsertIncome->execute([$currEmpId, $inc['income_master_id'], $inc['amount']]);
$addedIncomeAmount += $inc['amount'];
}
}
}
// Copy ONLY HOSPITAL deductions
$stmtGetDeductions->execute([$emp['id']]);
$deductions = $stmtGetDeductions->fetchAll();
foreach ($deductions as $ded) {
if (strtoupper($ded['fund_source']) === 'HOSPITAL') {
// Prevent duplicate insertion
$stmtCheckDeduction->execute([$currEmpId, $ded['deduction_master_id']]);
if (!$stmtCheckDeduction->fetch()) {
$stmtInsertDeduction->execute([$currEmpId, $ded['deduction_master_id'], $ded['amount']]);
$addedDeductionAmount += $ded['amount'];
}
}
}
// Update employee totals if anything was added
if ($addedIncomeAmount > 0 || $addedDeductionAmount > 0) {
$stmtUpdateTotals->execute([
$addedIncomeAmount,
$addedDeductionAmount,
$addedIncomeAmount, // Add to net
$addedDeductionAmount, // Subtract from net
$currEmpId
]);
}
$mergedCount++;
}
}
$this->commit();
return ['copied' => $copiedCount, 'merged' => $mergedCount];
} catch (\PDOException $e) {
$this->rollBack();
error_log("Error copying from previous month: " . $e->getMessage());
return false;
}
}
// --- Methods for Import ---
public function beginTransaction() {
if (!$this->db->inTransaction()) {
$this->db->beginTransaction();
}
}
public function commit() {
if ($this->db->inTransaction()) {
$this->db->commit();
}
}
public function rollBack() {
if ($this->db->inTransaction()) {
$this->db->rollBack();
}
}
public function getSalaryMonthById($id) {
$stmt = $this->db->prepare("SELECT sm.*, sy.year_no FROM salary_month sm JOIN salary_year sy ON sm.year_id = sy.id WHERE sm.id = ?");
$stmt->execute([$id]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
public function updateSalaryMonthStatus($id, $status) {
$stmt = $this->db->prepare("UPDATE salary_month SET status = ? WHERE id = ?");
return $stmt->execute([$status, $id]);
}
public function logImportHistory($userId, $salaryMonthId, $fileName, $total, $success, $error, $status = 'success') {
$sql = "INSERT INTO import_history (user_id, salary_month_id, file_name, status, total_records, success_records, error_records)
VALUES (?, ?, ?, ?, ?, ?, ?)";
$stmt = $this->db->prepare($sql);
$stmt->execute([$userId, $salaryMonthId, $fileName, $status, $total, $success, $error]);
return $this->db->lastInsertId();
}
public function insertEmployeeSalary($data) {
$check = $this->db->prepare("SELECT id FROM employee_salary WHERE salary_month_id = ? AND employee_code = ?");
$check->execute([$data['salary_month_id'], $data['employee_code']]);
$existing = $check->fetch(PDO::FETCH_ASSOC);
if ($existing) {
$sql = "UPDATE employee_salary SET national_id = ?, first_name = ?, last_name = ?,
position = ?, department = ?, base_salary = ?, total_income = ?, total_deduction = ?, net_salary = ?
WHERE id = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([
$data['national_id'], $data['first_name'], $data['last_name'],
$data['position'] ?? null, $data['department'] ?? null,
$data['base_salary'], $data['total_income'], $data['total_deduction'], $data['net_salary'],
$existing['id']
]);
// Delete old details
$this->db->prepare("DELETE FROM salary_income WHERE employee_salary_id = ?")->execute([$existing['id']]);
$this->db->prepare("DELETE FROM salary_deduction WHERE employee_salary_id = ?")->execute([$existing['id']]);
return $existing['id'];
} else {
$sql = "INSERT INTO employee_salary (salary_month_id, employee_code, national_id, first_name, last_name, position, department, base_salary, total_income, total_deduction, net_salary)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
$stmt = $this->db->prepare($sql);
$stmt->execute([
$data['salary_month_id'], $data['employee_code'], $data['national_id'],
$data['first_name'], $data['last_name'], $data['position'] ?? null, $data['department'] ?? null, $data['base_salary'],
$data['total_income'], $data['total_deduction'], $data['net_salary']
]);
return $this->db->lastInsertId();
}
}
public function insertSalaryIncome($employeeSalaryId, $incomeMasterId, $amount) {
$sql = "INSERT INTO salary_income (employee_salary_id, income_master_id, amount) VALUES (?, ?, ?)";
$stmt = $this->db->prepare($sql);
return $stmt->execute([$employeeSalaryId, $incomeMasterId, $amount]);
}
public function insertSalaryDeduction($employeeSalaryId, $deductionMasterId, $amount) {
$sql = "INSERT INTO salary_deduction (employee_salary_id, deduction_master_id, amount) VALUES (?, ?, ?)";
$stmt = $this->db->prepare($sql);
return $stmt->execute([$employeeSalaryId, $deductionMasterId, $amount]);
}
public function getPeriodStats($salaryMonthId) {
$stmt = $this->db->prepare("SELECT COUNT(id) as total_emp, SUM(net_salary) as total_net FROM employee_salary WHERE salary_month_id = ?");
$stmt->execute([$salaryMonthId]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
public function clearDataForPeriod($salaryMonthId) {
try {
$this->beginTransaction();
// Delete all employee salaries for this month (cascade will delete salary_income, salary_deduction, slip logs)
$stmt = $this->db->prepare("DELETE FROM employee_salary WHERE salary_month_id = ?");
$stmt->execute([$salaryMonthId]);
// Update status back to Draft
$this->updateSalaryMonthStatus($salaryMonthId, 'Draft');
$this->commit();
return true;
} catch (\Exception $e) {
$this->rollBack();
error_log("Error clearing data: " . $e->getMessage());
return false;
}
}
public function getEmployeesBySalaryMonthId($salaryMonthId) {
$stmt = $this->db->prepare("SELECT * FROM employee_salary WHERE salary_month_id = ? ORDER BY first_name ASC");
$stmt->execute([$salaryMonthId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
public function deleteEmployeeSalary($id) {
try {
$this->beginTransaction();
// Delete incomes and deductions first
$this->db->prepare("DELETE FROM salary_income WHERE employee_salary_id = ?")->execute([$id]);
$this->db->prepare("DELETE FROM salary_deduction WHERE employee_salary_id = ?")->execute([$id]);
// Delete main record
$stmt = $this->db->prepare("DELETE FROM employee_salary WHERE id = ?");
$stmt->execute([$id]);
$this->commit();
return true;
} catch (\Exception $e) {
$this->rollBack();
return false;
}
}
public function getEmployeeSalaryById($id) {
$stmt = $this->db->prepare("SELECT * FROM employee_salary WHERE id = ?");
$stmt->execute([$id]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
public function getEmployeeIncomes($employeeSalaryId) {
$stmt = $this->db->prepare("SELECT si.*, im.name, im.code, im.fund_source
FROM salary_income si
JOIN income_master im ON si.income_master_id = im.id
WHERE si.employee_salary_id = ?");
$stmt->execute([$employeeSalaryId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
public function getEmployeeDeductions($employeeSalaryId) {
$stmt = $this->db->prepare("SELECT sd.*, dm.name, dm.code, dm.fund_source
FROM salary_deduction sd
JOIN deduction_master dm ON sd.deduction_master_id = dm.id
WHERE sd.employee_salary_id = ?");
$stmt->execute([$employeeSalaryId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
public function updateEmployeeSalaryDetails($id, $baseSalary, $incomes, $deductions, $totalIncome, $totalDeduction, $netSalary) {
try {
$this->beginTransaction();
// 1. Update main record
$stmt = $this->db->prepare("UPDATE employee_salary
SET base_salary = ?, total_income = ?, total_deduction = ?, net_salary = ?
WHERE id = ?");
$stmt->execute([$baseSalary, $totalIncome, $totalDeduction, $netSalary, $id]);
// 2. Delete old incomes and deductions
$this->db->prepare("DELETE FROM salary_income WHERE employee_salary_id = ?")->execute([$id]);
$this->db->prepare("DELETE FROM salary_deduction WHERE employee_salary_id = ?")->execute([$id]);
// 3. Insert new incomes
if (!empty($incomes)) {
$stmtInc = $this->db->prepare("INSERT INTO salary_income (employee_salary_id, income_master_id, amount) VALUES (?, ?, ?)");
foreach ($incomes as $inc) {
$stmtInc->execute([$id, $inc['id'], $inc['amount']]);
}
}
// 4. Insert new deductions
if (!empty($deductions)) {
$stmtDed = $this->db->prepare("INSERT INTO salary_deduction (employee_salary_id, deduction_master_id, amount) VALUES (?, ?, ?)");
foreach ($deductions as $ded) {
$stmtDed->execute([$id, $ded['id'], $ded['amount']]);
}
}
$this->commit();
return true;
} catch (\Exception $e) {
$this->rollBack();
error_log("Error updating employee salary: " . $e->getMessage());
return false;
}
}
public function getSalariesByPeriod($month, $year) {
$stmt = $this->db->prepare("SELECT es.*, sm.month_no as month, sy.year_no as year
FROM employee_salary es
JOIN salary_month sm ON es.salary_month_id = sm.id
JOIN salary_year sy ON sm.year_id = sy.id
WHERE sm.month_no = ? AND sy.year_no = ?
ORDER BY es.first_name ASC");
$stmt->execute([$month, $year]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
public function getSalaryByNationalId($nationalId, $month, $year) {
$stmt = $this->db->prepare("SELECT es.*, sm.month_no as month, sy.year_no as year
FROM employee_salary es
JOIN salary_month sm ON es.salary_month_id = sm.id
JOIN salary_year sy ON sm.year_id = sy.id
WHERE REPLACE(es.national_id, '-', '') = ? AND sm.month_no = ? AND sy.year_no = ?");
$stmt->execute([$nationalId, $month, $year]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
public function getYearlyTaxSummary($nationalId, $year) {
$stmt = $this->db->prepare("
SELECT
SUM(es.total_income) as total_income,
SUM(
(SELECT SUM(si.amount) FROM salary_income si
JOIN income_master im ON si.income_master_id = im.id
WHERE si.employee_salary_id = es.id AND im.is_taxable = 1)
) as total_taxable_income,
SUM(
(SELECT SUM(sd.amount) FROM salary_deduction sd
JOIN deduction_master dm ON sd.deduction_master_id = dm.id
WHERE sd.employee_salary_id = es.id AND dm.code = 'TAX')
) as total_tax,
SUM(
(SELECT SUM(sd.amount) FROM salary_deduction sd
JOIN deduction_master dm ON sd.deduction_master_id = dm.id
WHERE sd.employee_salary_id = es.id AND dm.code IN ('FUND_A', 'FUND_B'))
) as total_ssf
FROM employee_salary es
JOIN salary_month sm ON es.salary_month_id = sm.id
JOIN salary_year sy ON sm.year_id = sy.id
WHERE REPLACE(es.national_id, '-', '') = ? AND sy.year_no = ? AND sm.status IN ('Locked', 'Published')
");
$stmt->execute([$nationalId, $year]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
public function getYearlySummary($year) {
$sql = "SELECT sm.month_no AS month,
SUM(e.total_income) AS sum_income,
SUM(e.total_deduction) AS sum_deduction
FROM salary_month sm
JOIN salary_year sy ON sm.year_id = sy.id
LEFT JOIN employee_salary e ON sm.id = e.salary_month_id
WHERE sy.year_no = :year
GROUP BY sm.month_no
ORDER BY sm.month_no ASC";
$stmt = $this->db->prepare($sql);
$stmt->bindParam(':year', $year, \PDO::PARAM_STR);
$stmt->execute();
$results = $stmt->fetchAll(\PDO::FETCH_ASSOC);
$summary = [];
foreach($results as $row) {
$summary[$row['month']] = [
'income' => (float)($row['sum_income'] ?? 0),
'deduction' => (float)($row['sum_deduction'] ?? 0)
];
}
return $summary;
}
public function getAvailableSlipsByNationalId($nationalId) {
$sql = "SELECT sm.month_no, sy.year_no
FROM employee_salary es
JOIN salary_month sm ON es.salary_month_id = sm.id
JOIN salary_year sy ON sm.year_id = sy.id
WHERE REPLACE(es.national_id, '-', '') = :nationalId AND sm.status IN ('Locked', 'Published')
ORDER BY sy.year_no DESC, sm.month_no DESC";
$stmt = $this->db->prepare($sql);
$stmt->bindParam(':nationalId', $nationalId, \PDO::PARAM_STR);
$stmt->execute();
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
public function getAvailableTaxesByNationalId($nationalId) {
$sql = "SELECT DISTINCT sy.year_no
FROM employee_salary es
JOIN salary_month sm ON es.salary_month_id = sm.id
JOIN salary_year sy ON sm.year_id = sy.id
WHERE REPLACE(es.national_id, '-', '') = :nationalId AND sm.status IN ('Locked', 'Published')
ORDER BY sy.year_no DESC";
$stmt = $this->db->prepare($sql);
$stmt->bindParam(':nationalId', $nationalId, \PDO::PARAM_STR);
$stmt->execute();
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
}
?>
@@ -0,0 +1,22 @@
<?php
namespace app\Models;
use PDO;
class SystemSettingModel extends Model {
public function getSetting($key, $default = '') {
$stmt = $this->db->prepare("SELECT setting_value FROM system_settings WHERE setting_key = ?");
$stmt->execute([$key]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ? $row['setting_value'] : $default;
}
public function setSetting($key, $value) {
$sql = "INSERT INTO system_settings (setting_key, setting_value)
VALUES (?, ?)
ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)";
$stmt = $this->db->prepare($sql);
return $stmt->execute([$key, $value]);
}
}
@@ -0,0 +1,92 @@
<?php
namespace app\Models;
use PDO;
class UserLogModel extends Model {
public function __construct() {
parent::__construct();
$this->initTable();
}
private function initTable() {
$sql = "CREATE TABLE IF NOT EXISTS user_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT,
action VARCHAR(255),
description TEXT,
ip_address VARCHAR(45),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
$this->db->exec($sql);
}
/**
* Log an action
*/
public function logAction($userId, $action, $description = '') {
$ip = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
// Handle Kiosk logs (null user_id) by assigning to the first admin user
// to prevent foreign key constraint violations
if ($userId === null) {
try {
$stmtUser = $this->db->query("SELECT id FROM users ORDER BY id ASC LIMIT 1");
$userId = $stmtUser->fetchColumn();
} catch (\Exception $e) {
$userId = 1;
}
if (!$userId) $userId = 1; // Fallback
}
$sql = "INSERT INTO user_logs (user_id, action, description, ip_address) VALUES (?, ?, ?, ?)";
try {
$stmt = $this->db->prepare($sql);
return $stmt->execute([$userId, $action, $description, $ip]);
} catch (\PDOException $e) {
// Prevent crashing if log insertion fails
return false;
}
}
/**
* Get logs with optional filters
*/
public function getLogs($filters = []) {
$sql = "SELECT l.*, u.username, u.first_name, u.last_name
FROM user_logs l
LEFT JOIN users u ON l.user_id = u.id
WHERE 1=1";
$params = [];
if (!empty($filters['user_id'])) {
$sql .= " AND l.user_id = ?";
$params[] = $filters['user_id'];
}
if (!empty($filters['action'])) {
$sql .= " AND l.action = ?";
$params[] = $filters['action'];
}
if (!empty($filters['date_start'])) {
$sql .= " AND DATE(l.created_at) >= ?";
$params[] = $filters['date_start'];
}
if (!empty($filters['date_end'])) {
$sql .= " AND DATE(l.created_at) <= ?";
$params[] = $filters['date_end'];
}
$sql .= " ORDER BY l.created_at DESC LIMIT 1000";
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
?>
@@ -0,0 +1,100 @@
<?php
namespace app\Models;
use PDO;
class UserModel extends Model {
/**
* Get user by username
*
* @param string $username
* @return array|false
*/
public function getUserByUsername($username) {
$stmt = $this->db->prepare("SELECT u.*, r.name as role_name FROM users u JOIN roles r ON u.role_id = r.id WHERE u.username = :username");
$stmt->bindParam(':username', $username);
$stmt->execute();
return $stmt->fetch(PDO::FETCH_ASSOC);
}
public function getUserById($id) {
$stmt = $this->db->prepare("SELECT u.*, r.name as role_name FROM users u JOIN roles r ON u.role_id = r.id WHERE u.id = :id");
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetch(PDO::FETCH_ASSOC);
}
public function getAllUsers() {
$stmt = $this->db->prepare("SELECT u.*, r.name as role_name FROM users u JOIN roles r ON u.role_id = r.id ORDER BY u.created_at DESC");
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
public function getRoles() {
$stmt = $this->db->prepare("SELECT * FROM roles ORDER BY id ASC");
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
public function createUser($data) {
$sql = "INSERT INTO users (username, password_hash, role_id, first_name, last_name, status)
VALUES (?, ?, ?, ?, ?, ?)";
$stmt = $this->db->prepare($sql);
return $stmt->execute([
$data['username'],
password_hash($data['password'], PASSWORD_DEFAULT),
$data['role_id'],
$data['first_name'],
$data['last_name'],
$data['status'] ?? 'active'
]);
}
/**
* Update last login time
*
* @param int $userId
*/
public function updateLastLogin($userId) {
$stmt = $this->db->prepare("UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = :id");
$stmt->bindParam(':id', $userId, PDO::PARAM_INT);
$stmt->execute();
}
public function updateUser($data) {
if (!empty($data['password'])) {
$sql = "UPDATE users SET first_name = ?, last_name = ?, role_id = ?, password_hash = ? WHERE id = ?";
$stmt = $this->db->prepare($sql);
return $stmt->execute([
$data['first_name'],
$data['last_name'],
$data['role_id'],
password_hash($data['password'], PASSWORD_DEFAULT),
$data['id']
]);
} else {
$sql = "UPDATE users SET first_name = ?, last_name = ?, role_id = ? WHERE id = ?";
$stmt = $this->db->prepare($sql);
return $stmt->execute([
$data['first_name'],
$data['last_name'],
$data['role_id'],
$data['id']
]);
}
}
public function updateStatus($userId, $status) {
$sql = "UPDATE users SET status = ? WHERE id = ?";
$stmt = $this->db->prepare($sql);
return $stmt->execute([$status, $userId]);
}
public function updatePassword($userId, $newPassword) {
$sql = "UPDATE users SET password_hash = ? WHERE id = ?";
$stmt = $this->db->prepare($sql);
return $stmt->execute([password_hash($newPassword, PASSWORD_DEFAULT), $userId]);
}
}
?>
@@ -0,0 +1,205 @@
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>เข้าสู่ระบบ | <?= htmlspecialchars(APP_NAME) ?></title>
<link rel="icon" type="image/png" href="<?= BASE_URL ?>/public/favicon.png">
<link rel="shortcut icon" type="image/x-icon" href="<?= BASE_URL ?>/public/favicon.ico">
<!-- Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'sans-serif'],
},
colors: {
primary: '#0ea5e9',
secondary: '#14b8a6',
dark: '#0f172a'
}
}
}
}
</script>
<!-- Google Fonts: Inter -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<!-- Custom CSS -->
<link rel="stylesheet" href="<?= BASE_URL ?>/public/assets/css/style.css">
<style>
body {
display: flex;
align-items: center;
justify-content: center;
}
.login-container {
width: 100%;
max-width: 450px;
padding: 15px;
}
.login-logo {
width: 80px;
height: 80px;
background: rgba(255, 255, 255, 0.5);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 20px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
.login-logo svg {
width: 40px;
height: 40px;
color: var(--primary-color);
}
</style>
</head>
<body class="h-screen w-screen overflow-hidden text-slate-800 dark:text-slate-200">
<!-- Full Screen Background Image -->
<div class="absolute inset-0 z-0 bg-cover bg-center" style="background-image: url('<?= BASE_URL ?>/public/assets/images/hospital_background_1783495310883.jpg');">
<!-- Overlay -->
<div class="absolute inset-0 z-10 bg-overlay backdrop-blur-sm"></div>
</div>
<!-- Login Container -->
<div class="relative z-20 flex items-center justify-center h-full px-4">
<div class="glass-card rounded-3xl p-8 md:p-12 w-full max-w-md shadow-2xl animate-float transition-all duration-300">
<!-- Logo Section -->
<div class="text-center mb-8">
<img src="<?= BASE_URL ?>/public/img/logo.png" alt="Logo" class="w-24 h-24 object-contain mx-auto mb-4">
<h1 class="text-2xl font-bold text-slate-900 dark:text-white"><?= APP_NAME ?></h1>
<p class="text-sm text-slate-600 dark:text-slate-400 mt-2">โรงพยาบาลเกาะสมุย (Koh Samui Hospital)</p>
</div>
<!-- Login Form -->
<form id="loginForm" action="<?= BASE_URL ?>/login" method="POST" class="space-y-6">
<div>
<label for="username" class="block text-sm font-medium mb-2">ชื่อผู้ใช้งาน</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-slate-400">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" /></svg>
</div>
<input type="text" id="username" name="username" class="block w-full pl-10 pr-3 py-3 border border-slate-300 dark:border-slate-600 rounded-xl focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary bg-white/50 dark:bg-slate-800/50 transition-all" placeholder="กรอกชื่อผู้ใช้งาน" required>
</div>
</div>
<div>
<label for="password" class="block text-sm font-medium mb-2">รหัสผ่าน</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-slate-400">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z" /></svg>
</div>
<input type="password" id="password" name="password" class="block w-full pl-10 pr-10 py-3 border border-slate-300 dark:border-slate-600 rounded-xl focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary bg-white/50 dark:bg-slate-800/50 transition-all" placeholder="กรอกรหัสผ่าน" required>
<button type="button" id="togglePassword" class="absolute inset-y-0 right-0 pr-3 flex items-center text-slate-400 hover:text-primary transition-colors">
<svg id="eyeIcon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /></svg>
</button>
</div>
</div>
<button type="submit" id="submitBtn" class="w-full flex justify-center py-3 px-4 border border-transparent rounded-xl shadow-sm text-sm font-bold text-white bg-gradient-to-r from-primary to-secondary hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary transition-all group">
<span id="btnText">เข้าสู่ระบบ</span>
<svg id="btnSpinner" class="hidden animate-spin ml-2 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
</button>
</form>
<!-- Dark Mode Toggle & Footer -->
<div class="mt-8 pt-6 border-t border-slate-300 dark:border-slate-600 flex justify-between items-center">
<span class="text-xs text-slate-500">v<?= htmlspecialchars((new \app\Models\SystemSettingModel())->getSetting('app_version', '1.0.0')) ?></span>
<button id="themeToggle" class="text-slate-500 hover:text-primary transition-colors p-2 rounded-full hover:bg-slate-200 dark:hover:bg-slate-700">
<svg id="themeIcon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z" /></svg>
</button>
</div>
</div>
</div>
<!-- SweetAlert2 & JS -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Theme Toggle Logic
const themeToggle = document.getElementById('themeToggle');
const themeIcon = document.getElementById('themeIcon');
const html = document.documentElement;
// Check local storage for theme
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
html.classList.add('dark');
themeIcon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" />';
} else {
html.classList.remove('dark');
}
themeToggle.addEventListener('click', () => {
if (html.classList.contains('dark')) {
html.classList.remove('dark');
localStorage.theme = 'light';
themeIcon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z" />';
} else {
html.classList.add('dark');
localStorage.theme = 'dark';
themeIcon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" />';
}
});
// Show/Hide Password Logic
const togglePassword = document.getElementById('togglePassword');
const password = document.getElementById('password');
const eyeIcon = document.getElementById('eyeIcon');
togglePassword.addEventListener('click', function (e) {
const type = password.getAttribute('type') === 'password' ? 'text' : 'password';
password.setAttribute('type', type);
if (type === 'text') {
eyeIcon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.451 10.451 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.522 10.522 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.894 7.894L21 21m-3.228-3.228-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88" />';
} else {
eyeIcon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />';
}
});
// Loading State Logic
const form = document.getElementById('loginForm');
const submitBtn = document.getElementById('submitBtn');
const btnText = document.getElementById('btnText');
const btnSpinner = document.getElementById('btnSpinner');
form.addEventListener('submit', function() {
submitBtn.disabled = true;
btnText.textContent = 'กำลังเข้าสู่ระบบ...';
btnSpinner.classList.remove('hidden');
submitBtn.classList.add('opacity-75', 'cursor-not-allowed');
});
// Alerts
const Toast = Swal.mixin({
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 3000,
timerProgressBar: true,
});
<?php if (isset($_SESSION['error'])): ?>
Toast.fire({
icon: 'error',
title: '<?= htmlspecialchars($_SESSION['error'], ENT_QUOTES, 'UTF-8') ?>'
});
<?php unset($_SESSION['error']); ?>
<?php endif; ?>
<?php if (isset($_SESSION['success'])): ?>
Toast.fire({
icon: 'success',
title: '<?= htmlspecialchars($_SESSION['success'], ENT_QUOTES, 'UTF-8') ?>'
});
<?php unset($_SESSION['success']); ?>
<?php endif; ?>
});
</script>
</body>
</html>
@@ -0,0 +1,147 @@
<?php require_once APP_ROOT . '/app/Views/layouts/header.php'; ?>
<div class="mb-6">
<h2 class="text-2xl font-bold text-slate-800 dark:text-white">Dashboard (แผงควบคุม)</h2>
<p class="text-slate-500 dark:text-slate-400">ยินดีต้อนรับเข้าสู่ระบบจัดการข้อมูลเงินเดือนเจ้าหน้าที่</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6 mb-8">
<div class="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-sm border-l-4 border-sky-500 flex flex-col items-center justify-center transition-transform hover:-translate-y-1">
<h6 class="text-slate-500 dark:text-slate-400 text-xs font-bold uppercase tracking-wider mb-2">บุคลากรทั้งหมด (งวดล่าสุด)</h6>
<h2 class="text-sky-500 text-3xl font-extrabold m-0"><?= number_format($totalEmployees ?? 0) ?></h2>
<p class="text-slate-400 text-xs m-0 mt-1">คน</p>
</div>
<div class="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-sm border-l-4 border-teal-500 flex flex-col items-center justify-center transition-transform hover:-translate-y-1">
<h6 class="text-slate-500 dark:text-slate-400 text-xs font-bold uppercase tracking-wider mb-2">ยอดเงินเดือนสุทธิ (งวดล่าสุด)</h6>
<h2 class="text-teal-500 text-3xl font-extrabold m-0"><?= number_format($totalSalary ?? 0, 2) ?></h2>
<p class="text-slate-400 text-xs m-0 mt-1">บาท</p>
</div>
<div class="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-sm border-l-4 border-amber-500 flex flex-col items-center justify-center transition-transform hover:-translate-y-1">
<h6 class="text-slate-500 dark:text-slate-400 text-xs font-bold uppercase tracking-wider mb-2">สถานะงวดล่าสุด</h6>
<?php if ($latestPeriod): ?>
<h2 class="text-amber-500 text-xl font-extrabold m-0"><?= htmlspecialchars($latestPeriod['status']) ?></h2>
<p class="text-slate-400 text-xs m-0 mt-1">เดือน <?= $latestPeriod['month_no'] ?>/<?= $latestPeriod['year_no'] > 2500 ? $latestPeriod['year_no'] : $latestPeriod['year_no'] + 543 ?></p>
<?php else: ?>
<h2 class="text-slate-500 text-xl font-extrabold m-0">ไม่มีข้อมูล</h2>
<?php endif; ?>
</div>
<div class="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-sm flex flex-col justify-center">
<h6 class="text-slate-500 dark:text-slate-400 text-xs font-bold uppercase tracking-wider mb-4 text-center">เมนูด่วน</h6>
<a href="<?= BASE_URL ?>/payroll" class="w-full flex items-center justify-center px-4 py-2 bg-sky-500 text-white rounded-xl hover:bg-sky-600 transition-colors mb-3 font-medium text-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v6m3-3H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" /></svg>
จัดการงวดเงินเดือน
</a>
<a href="<?= BASE_URL ?>/import" class="w-full flex items-center justify-center px-4 py-2 bg-white dark:bg-slate-700 border border-slate-300 dark:border-slate-600 text-slate-700 dark:text-slate-200 rounded-xl hover:bg-slate-50 dark:hover:bg-slate-600 transition-colors font-medium text-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5" /></svg>
นำเข้าข้อมูล
</a>
</div>
</div>
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-sm p-6 mb-8">
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-6 gap-4">
<div class="flex items-center">
<div class="p-2 bg-sky-100 dark:bg-sky-900/40 rounded-lg text-sky-500 mr-3">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 18 9 11.25l4.306 4.306a11.95 11.95 0 0 1 5.814-5.518l2.74-1.22m0 0-5.94-2.281m5.94 2.28-2.28 5.941" /></svg>
</div>
<h5 class="font-bold text-lg text-slate-800 dark:text-white m-0">ภาพรวมรายรับ-รายจ่าย ประจำปี พ.ศ. <?= htmlspecialchars($yearBE ?? date('Y')+543) ?></h5>
</div>
<div>
<form action="<?= BASE_URL ?>/dashboard" method="GET" class="flex items-center">
<label for="year" class="text-sm font-medium text-slate-600 dark:text-slate-400 mr-2">เลือกปี:</label>
<select name="year" id="year" onchange="this.form.submit()" class="rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-200 px-3 py-1.5 focus:outline-none focus:ring-2 focus:ring-sky-500 transition-all text-sm font-medium cursor-pointer">
<?php if (isset($availableYears) && is_array($availableYears)): ?>
<?php foreach ($availableYears as $y): ?>
<option value="<?= $y ?>" <?= ($currentYear == $y) ? 'selected' : '' ?>>พ.ศ. <?= $y + 543 ?></option>
<?php endforeach; ?>
<?php endif; ?>
</select>
</form>
</div>
</div>
<div class="w-full bg-slate-50 dark:bg-slate-900/50 rounded-xl border border-slate-100 dark:border-slate-700 p-4 h-80 relative">
<canvas id="payrollChart"></canvas>
</div>
</div>
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const ctx = document.getElementById('payrollChart').getContext('2d');
const isDark = document.documentElement.classList.contains('dark');
const textColor = isDark ? '#94a3b8' : '#64748b';
const gridColor = isDark ? '#334155' : '#e2e8f0';
new Chart(ctx, {
type: 'bar',
data: {
labels: <?= $chartLabels ?? '[]' ?>,
datasets: [
{
label: 'รายรับรวม (บาท)',
data: <?= $chartIncome ?? '[]' ?>,
backgroundColor: '#10b981', // emerald-500
borderRadius: 4,
},
{
label: 'รายจ่ายรวม (บาท)',
data: <?= $chartDeduction ?? '[]' ?>,
backgroundColor: '#ef4444', // red-500
borderRadius: 4,
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
labels: {
color: textColor
}
},
tooltip: {
callbacks: {
label: function(context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
label += new Intl.NumberFormat('th-TH', { style: 'currency', currency: 'THB' }).format(context.parsed.y);
}
return label;
}
}
}
},
scales: {
y: {
grid: { color: gridColor },
ticks: {
color: textColor,
callback: function(value, index, values) {
if(value >= 1000000) return (value / 1000000) + 'M';
if(value >= 1000) return (value / 1000) + 'k';
return value;
}
}
},
x: {
grid: { display: false },
ticks: { color: textColor }
}
}
}
});
});
</script>
<?php require_once APP_ROOT . '/app/Views/layouts/footer.php'; ?>
@@ -0,0 +1,91 @@
<?php require_once APP_ROOT . '/app/Views/layouts/header.php'; ?>
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-6 gap-4">
<div>
<h2 class="text-2xl font-bold text-slate-800 dark:text-white">นำเข้าข้อมูลเงินเดือน (Import CSV)</h2>
<p class="text-slate-500 dark:text-slate-400 text-sm mt-1">อัปโหลดไฟล์ CSV เพื่อนำเข้าข้อมูลเงินเดือนของเจ้าหน้าที่</p>
</div>
<a href="#" class="inline-flex items-center justify-center px-4 py-2.5 bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-600 text-slate-700 dark:text-slate-300 text-sm font-medium rounded-xl hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors shadow-sm w-full md:w-auto">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" /></svg>
ดาวน์โหลด Template
</a>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="lg:col-span-2">
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-100 dark:border-slate-700 p-6">
<form action="<?= BASE_URL ?>/import/upload" method="POST" enctype="multipart/form-data">
<div class="mb-6">
<label for="salary_month_id" class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2">เลือกงวดเงินเดือน</label>
<select id="salary_month_id" name="salary_month_id" required class="block w-full rounded-xl border-0 py-3 px-4 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-sky-500 bg-slate-50 dark:bg-slate-900 sm:text-sm">
<option value="">-- เลือกงวดเงินเดือนที่ต้องการนำเข้า --</option>
<?php foreach ($periods as $period): ?>
<option value="<?= $period['id'] ?>">งวดเดือน <?= $period['month_no'] ?>/<?= $period['year_no'] > 2500 ? $period['year_no'] : $period['year_no'] + 543 ?> (<?= $period['status'] ?>)</option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-6">
<label for="file_type" class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2">รูปแบบไฟล์ (File Format)</label>
<select id="file_type" name="file_type" required class="block w-full rounded-xl border-0 py-3 px-4 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-sky-500 bg-slate-50 dark:bg-slate-900 sm:text-sm">
<option value="csv">ไฟล์ CSV มาตรฐาน (Standard Template)</option>
<option value="cgd_txt">ไฟล์จากกรมบัญชีกลาง (.txt)</option>
</select>
</div>
<div class="mb-6">
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2">อัปโหลดไฟล์</label>
<div class="mt-2 flex justify-center rounded-xl border border-dashed border-slate-300 dark:border-slate-600 px-6 py-10 bg-slate-50 dark:bg-slate-900/50 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors cursor-pointer" onclick="document.getElementById('csv_file').click()">
<div class="text-center">
<svg class="mx-auto h-12 w-12 text-slate-300 dark:text-slate-600" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M1.5 6a2.25 2.25 0 012.25-2.25h16.5A2.25 2.25 0 0122.5 6v12a2.25 2.25 0 01-2.25 2.25H3.75A2.25 2.25 0 011.5 18V6zM3 16.06V18c0 .414.336.75.75.75h16.5A.75.75 0 0021 18v-1.94l-2.69-2.689a1.5 1.5 0 00-2.12 0l-.88.879.97.97a.75.75 0 11-1.06 1.06l-5.16-5.159a1.5 1.5 0 00-2.12 0L3 16.061zm10.125-7.81a1.125 1.125 0 112.25 0 1.125 1.125 0 01-2.25 0z" clip-rule="evenodd" />
</svg>
<div class="mt-4 flex text-sm leading-6 text-slate-600 dark:text-slate-400 justify-center">
<label for="csv_file" class="relative cursor-pointer rounded-md bg-transparent font-semibold text-sky-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-sky-600 focus-within:ring-offset-2 hover:text-sky-400">
<span>คลิกเพื่อเลือกไฟล์</span>
<input id="csv_file" name="csv_file" type="file" class="sr-only" accept=".csv, .txt" required onchange="document.getElementById('fileNameDisplay').textContent = this.files[0].name">
</label>
<p class="pl-1">หรือลากไฟล์มาวางที่นี่</p>
</div>
<p class="text-xs leading-5 text-slate-500 dark:text-slate-500 mt-2">รองรับเฉพาะไฟล์ .csv หรือ .txt ตามรูปแบบที่เลือก</p>
<p id="fileNameDisplay" class="mt-3 text-sm font-bold text-slate-700 dark:text-slate-300"></p>
</div>
</div>
</div>
<div class="flex items-center justify-end gap-x-6">
<button type="submit" class="rounded-xl bg-sky-500 px-6 py-3 text-sm font-semibold text-white shadow-sm hover:bg-sky-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-600 transition-colors">
เริ่มนำเข้าข้อมูล
</button>
</div>
</form>
</div>
</div>
<div>
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-100 dark:border-slate-700 p-6">
<h3 class="text-lg font-bold text-slate-800 dark:text-white mb-4 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 mr-2 text-amber-500"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2.25m0 4.5h.01M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" /></svg>
คำแนะนำในการนำเข้า
</h3>
<ul class="space-y-3 text-sm text-slate-600 dark:text-slate-400">
<li class="flex items-start">
<span class="mr-2 mt-0.5 text-sky-500">•</span>
<span><b>กรณีไฟล์ CSV ปกติ:</b> กรุณาตรวจสอบคอลัมน์ในไฟล์ CSV ให้ตรงกับ Template ของระบบ ชื่อคอลัมน์จะต้องเหมือนกับ รหัสรายรับ และ รหัสรายจ่าย</span>
</li>
<li class="flex items-start">
<span class="mr-2 mt-0.5 text-sky-500">•</span>
<span><b>กรณีไฟล์กรมบัญชีกลาง:</b> ระบบจะใช้อ่านไฟล์ .txt (รหัส TIS-620/Windows-874) โดยอิงตามเลขฟิลด์มาตรฐานของกรมฯ</span>
</li>
<li class="flex items-start">
<span class="mr-2 mt-0.5 text-sky-500">•</span>
<span>หากเกิดข้อผิดพลาด ระบบจะบันทึกเฉพาะรายการที่นำเข้าสำเร็จหรือ Rollback กลับทั้งหมด</span>
</li>
</ul>
</div>
</div>
</div>
<?php require_once APP_ROOT . '/app/Views/layouts/footer.php'; ?>
@@ -0,0 +1,417 @@
<!DOCTYPE html>
<html lang="th" class="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kiosk - พิมพ์สลิปเงินเดือนและใบเสียภาษี</title>
<link rel="icon" type="image/png" href="<?= BASE_URL ?>/public/favicon.png">
<link rel="shortcut icon" type="image/x-icon" href="<?= BASE_URL ?>/public/favicon.ico">
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Kanit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
sans: ['Kanit', 'sans-serif'],
}
}
}
}
</script>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<style>
.kiosk-btn {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
background: linear-gradient(to bottom, #ffffff, #f1f5f9);
border: 1px solid #cbd5e1;
border-bottom-width: 6px;
border-radius: 1rem;
font-size: 2.5rem;
font-weight: 700;
color: #334155;
box-shadow: 0 4px 6px rgba(0,0,0,0.1), inset 0 -2px 0 rgba(0,0,0,0.05);
transition: all 0.1s ease;
cursor: pointer;
user-select: none;
height: 5.5rem;
}
.kiosk-btn:active {
transform: translateY(4px);
border-bottom-width: 2px;
margin-top: 4px;
box-shadow: 0 1px 2px rgba(0,0,0,0.1), inset 0 2px 4px rgba(0,0,0,0.1);
background: #f8fafc;
}
.kiosk-btn.btn-danger {
color: #ef4444;
background: linear-gradient(to bottom, #fef2f2, #fee2e2);
border-color: #fca5a5;
border-bottom-color: #f87171;
}
.kiosk-btn.btn-danger:active {
background: #fee2e2;
}
.kiosk-btn.btn-gray {
color: #64748b;
background: linear-gradient(to bottom, #f8fafc, #f1f5f9);
border-color: #cbd5e1;
border-bottom-color: #94a3b8;
}
.kiosk-btn.btn-gray:active {
background: #f1f5f9;
}
.digit-box {
@apply w-8 h-12 md:w-11 md:h-14 flex items-center justify-center text-3xl font-bold bg-white border-2 border-slate-200 rounded-lg shadow-inner text-slate-800 transition-colors;
}
.digit-box.filled {
@apply border-sky-400 bg-sky-50 text-sky-700 shadow-sm;
}
.digit-box.active {
@apply border-sky-500 ring-2 ring-sky-200 shadow-sm scale-110 transform transition-transform;
}
.numpad-container {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
}
/* Hide scrollbar for clean kiosk look */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #f1f5f9;
}
::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
</style>
</head>
<body class="bg-gradient-to-br from-slate-100 to-slate-200 min-h-screen flex flex-col items-center justify-center p-4 sm:p-8">
<div class="w-full max-w-6xl flex flex-col md:flex-row gap-8 bg-white/80 backdrop-blur-xl p-8 rounded-[2.5rem] shadow-2xl border border-white/50">
<!-- Left Side: Numpad -->
<div class="flex-1 flex flex-col items-center border-b md:border-b-0 md:border-r border-slate-200 pb-8 md:pb-0 md:pr-8">
<h1 class="text-3xl font-bold text-slate-800 mb-2">ตรวจสอบข้อมูลเอกสาร</h1>
<p class="text-slate-500 mb-8 text-center text-lg">กรุณาระบุเลขประจำตัวประชาชน 13 หลัก</p>
<div class="w-full max-w-lg mb-8 relative flex justify-center gap-1.5 md:gap-2 items-center" id="digit_container">
<div class="digit-box active" id="digit-0"></div>
<div class="text-xl md:text-2xl font-bold text-slate-300">-</div>
<div class="digit-box" id="digit-1"></div>
<div class="digit-box" id="digit-2"></div>
<div class="digit-box" id="digit-3"></div>
<div class="digit-box" id="digit-4"></div>
<div class="text-xl md:text-2xl font-bold text-slate-300">-</div>
<div class="digit-box" id="digit-5"></div>
<div class="digit-box" id="digit-6"></div>
<div class="digit-box" id="digit-7"></div>
<div class="digit-box" id="digit-8"></div>
<div class="digit-box" id="digit-9"></div>
<div class="text-xl md:text-2xl font-bold text-slate-300">-</div>
<div class="digit-box" id="digit-10"></div>
<div class="digit-box" id="digit-11"></div>
<div class="text-xl md:text-2xl font-bold text-slate-300">-</div>
<div class="digit-box" id="digit-12"></div>
</div>
<div class="w-full max-w-sm numpad-container">
<button type="button" class="kiosk-btn" onclick="pressKey('1')">1</button>
<button type="button" class="kiosk-btn" onclick="pressKey('2')">2</button>
<button type="button" class="kiosk-btn" onclick="pressKey('3')">3</button>
<button type="button" class="kiosk-btn" onclick="pressKey('4')">4</button>
<button type="button" class="kiosk-btn" onclick="pressKey('5')">5</button>
<button type="button" class="kiosk-btn" onclick="pressKey('6')">6</button>
<button type="button" class="kiosk-btn" onclick="pressKey('7')">7</button>
<button type="button" class="kiosk-btn" onclick="pressKey('8')">8</button>
<button type="button" class="kiosk-btn" onclick="pressKey('9')">9</button>
<button type="button" class="kiosk-btn btn-danger" onclick="clearKey()">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-8 h-8"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
</button>
<button type="button" class="kiosk-btn" onclick="pressKey('0')">0</button>
<button type="button" class="kiosk-btn btn-gray" onclick="backspaceKey()">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-8 h-8"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9.75L14.25 12m0 0l2.25 2.25M14.25 12l2.25-2.25M14.25 12L12 14.25m-2.58 4.92l-6.375-6.375a1.125 1.125 0 010-1.59L9.42 4.83c.211-.211.498-.33.796-.33H19.5a2.25 2.25 0 012.25 2.25v10.5a2.25 2.25 0 01-2.25 2.25h-9.284c-.298 0-.585-.119-.796-.33z" /></svg>
</button>
</div>
</div>
<!-- Right Side: Results -->
<div class="flex-1 flex flex-col" id="results_pane">
<div class="flex-1 flex flex-col items-center justify-center text-center opacity-50" id="waiting_state">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1" stroke="currentColor" class="w-32 h-32 text-slate-300 mb-6"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" /></svg>
<p class="text-2xl font-medium text-slate-600">รอการระบุข้อมูล</p>
<p class="text-slate-500 mt-2">พิมพ์รหัสบัตรประชาชนเพื่อค้นหาเอกสาร</p>
</div>
<div class="flex-1 hidden flex-col" id="data_state">
<div class="bg-sky-50 rounded-2xl p-6 mb-6">
<p class="text-sky-600 font-medium text-sm">ผู้ใช้งาน</p>
<h2 class="text-2xl font-bold text-slate-800 mt-1" id="person_name">ชื่อ - สกุล</h2>
</div>
<!-- Tabs -->
<div class="flex gap-2 mb-6 p-1 bg-slate-100 rounded-xl">
<button class="flex-1 py-3 text-center rounded-lg font-semibold bg-white text-sky-600 shadow-sm transition-all" id="tab_slip" onclick="switchTab('slip')">สลิปเงินเดือน</button>
<button class="flex-1 py-3 text-center rounded-lg font-semibold text-slate-500 hover:text-slate-700 transition-all" id="tab_tax" onclick="switchTab('tax')">ใบเสียภาษี 50 ทวิ</button>
</div>
<div class="flex-1 overflow-y-auto pr-2 relative flex flex-col">
<!-- Slips List -->
<div id="slips_list_container" class="flex flex-col h-full">
<div id="slips_list" class="grid grid-cols-1 sm:grid-cols-2 gap-4 flex-1">
<!-- Rendered via JS -->
</div>
<div class="mt-4 pt-4 border-t border-slate-200 hidden" id="slip_bulk_action">
<button onclick="printSelectedSlips()" class="w-full py-4 bg-sky-500 hover:bg-sky-600 text-white rounded-xl font-semibold text-lg transition-colors flex items-center justify-center gap-2 shadow-md hover:shadow-lg">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" d="M6.72 13.829c-.24.03-.48.062-.72.096m.72-.096a42.415 42.415 0 0110.56 0m-10.56 0L6.34 18m10.94-4.171c.24.03.48.062.72.096m-.72-.096L17.66 18m0 0l.229 2.523a1.125 1.125 0 01-1.12 1.227H7.231c-.662 0-1.18-.568-1.12-1.227L6.34 18m11.318 0h1.091A2.25 2.25 0 0021 15.75V9.456c0-1.081-.768-2.015-1.837-2.175a48.055 48.055 0 00-1.913-.247M6.34 18H5.25A2.25 2.25 0 013 15.75V9.456c0-1.081.768-2.015 1.837-2.175a48.041 48.041 0 011.913-.247m10.5 0a48.536 48.536 0 00-10.5 0v2.756a2.25 2.25 0 002.25 2.25h6a2.25 2.25 0 002.25-2.25V9.229z" /></svg>
พิมพ์สลิปที่เลือก
</button>
</div>
</div>
<!-- Tax List -->
<div id="taxes_list" class="hidden grid grid-cols-1 sm:grid-cols-2 gap-4">
<!-- Rendered via JS -->
</div>
</div>
</div>
</div>
</div>
<!-- Alert Modal -->
<div id="alert_modal" class="fixed inset-0 bg-slate-900/50 backdrop-blur-sm z-50 flex items-center justify-center hidden opacity-0 transition-opacity">
<div class="bg-white rounded-3xl p-8 max-w-sm w-full mx-4 text-center transform scale-95 transition-transform duration-300" id="alert_content">
<div class="w-20 h-20 bg-red-100 text-red-500 rounded-full flex items-center justify-center mx-auto mb-6">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-10 h-10"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
</div>
<h3 class="text-2xl font-bold text-slate-800 mb-2">ไม่พบข้อมูล</h3>
<p class="text-slate-500 mb-8" id="alert_message">กรุณาตรวจสอบรหัสบัตรประชาชนอีกครั้ง</p>
<button onclick="closeAlert()" class="w-full py-4 bg-slate-800 hover:bg-slate-700 text-white rounded-xl font-semibold text-lg transition-colors">ตกลง</button>
</div>
</div>
<script>
const nidInput = document.getElementById('nid_input');
let currentNid = '';
const thaiMonths = {
1: 'มกราคม', 2: 'กุมภาพันธ์', 3: 'มีนาคม', 4: 'เมษายน',
5: 'พฤษภาคม', 6: 'มิถุนายน', 7: 'กรกฎาคม', 8: 'สิงหาคม',
9: 'กันยายน', 10: 'ตุลาคม', 11: 'พฤศจิกายน', 12: 'ธันวาคม'
};
function pressKey(num) {
if (currentNid.length < 13) {
currentNid += num;
updateDisplay();
if (currentNid.length === 13) {
searchData();
}
}
}
function backspaceKey() {
if (currentNid.length > 0) {
currentNid = currentNid.slice(0, -1);
updateDisplay();
resetUI();
}
}
function clearKey() {
currentNid = '';
updateDisplay();
resetUI();
}
function updateDisplay() {
for (let i = 0; i < 13; i++) {
let el = document.getElementById('digit-' + i);
if (i < currentNid.length) {
el.innerText = currentNid[i];
el.classList.add('filled');
el.classList.remove('active');
} else if (i === currentNid.length) {
el.innerText = '';
el.classList.remove('filled');
el.classList.add('active');
} else {
el.innerText = '';
el.classList.remove('filled');
el.classList.remove('active');
}
}
}
function resetUI() {
$('#waiting_state').removeClass('hidden');
$('#data_state').addClass('hidden');
}
function showAlert(msg) {
$('#alert_message').text(msg);
const modal = $('#alert_modal');
modal.removeClass('hidden');
setTimeout(() => {
modal.removeClass('opacity-0');
$('#alert_content').removeClass('scale-95');
}, 10);
}
function closeAlert() {
const modal = $('#alert_modal');
modal.addClass('opacity-0');
$('#alert_content').addClass('scale-95');
setTimeout(() => {
modal.addClass('hidden');
clearKey();
}, 300);
}
function switchTab(tab) {
if (tab === 'slip') {
$('#tab_slip').addClass('bg-white text-sky-600 shadow-sm').removeClass('text-slate-500 hover:text-slate-700');
$('#tab_tax').removeClass('bg-white text-sky-600 shadow-sm').addClass('text-slate-500 hover:text-slate-700');
$('#slips_list_container').removeClass('hidden');
$('#taxes_list').addClass('hidden');
} else {
$('#tab_tax').addClass('bg-white text-sky-600 shadow-sm').removeClass('text-slate-500 hover:text-slate-700');
$('#tab_slip').removeClass('bg-white text-sky-600 shadow-sm').addClass('text-slate-500 hover:text-slate-700');
$('#taxes_list').removeClass('hidden');
$('#slips_list_container').addClass('hidden');
}
}
function searchData() {
$('#waiting_state').html(`
<div class="animate-spin rounded-full h-16 w-16 border-t-4 border-b-4 border-sky-500 mb-6"></div>
<p class="text-xl font-medium text-slate-600">กำลังค้นหาข้อมูล...</p>
`);
$.post('<?= BASE_URL ?>/kiosk/search', { national_id: currentNid }, function(response) {
if (response.status === 'success') {
$('#person_name').text(response.data.name);
// Render Slips
let slipsHtml = '';
if (response.data.slips.length > 0) {
response.data.slips.forEach(slip => {
let monthName = thaiMonths[parseInt(slip.month_no)];
let yearTh = parseInt(slip.year_no) > 2500 ? parseInt(slip.year_no) : parseInt(slip.year_no) + 543;
let paddedMonth = slip.month_no.toString().padStart(2, '0');
slipsHtml += `
<label class="flex items-center justify-between p-4 bg-white border border-slate-200 rounded-2xl cursor-pointer hover:border-sky-300 hover:shadow-md transition-all group has-[:checked]:border-sky-500 has-[:checked]:bg-sky-50/50 has-[:checked]:ring-1 has-[:checked]:ring-sky-500">
<div class="flex items-center gap-4">
<div class="relative flex items-center">
<input type="checkbox" class="slip-checkbox peer h-6 w-6 cursor-pointer appearance-none rounded-md border-2 border-slate-300 checked:border-sky-500 checked:bg-sky-500 transition-all" value="${paddedMonth}-${slip.year_no}">
<svg class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-4 h-4 text-white opacity-0 peer-checked:opacity-100 pointer-events-none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>
</div>
<div>
<p class="font-semibold text-slate-800 text-lg">เดือน ${monthName}</p>
<p class="text-sm text-slate-500">พ.ศ. ${yearTh}</p>
</div>
</div>
<a href="<?= BASE_URL ?>/kiosk/print/slip/${currentNid}/${paddedMonth}/${slip.year_no}" target="_blank" onclick="event.stopPropagation();" class="w-10 h-10 rounded-full bg-slate-50 flex items-center justify-center text-slate-400 hover:bg-sky-100 hover:text-sky-600 transition-colors" title="พิมพ์เดี่ยว">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M6.72 13.829c-.24.03-.48.062-.72.096m.72-.096a42.415 42.415 0 0110.56 0m-10.56 0L6.34 18m10.94-4.171c.24.03.48.062.72.096m-.72-.096L17.66 18m0 0l.229 2.523a1.125 1.125 0 01-1.12 1.227H7.231c-.662 0-1.18-.568-1.12-1.227L6.34 18m11.318 0h1.091A2.25 2.25 0 0021 15.75V9.456c0-1.081-.768-2.015-1.837-2.175a48.055 48.055 0 00-1.913-.247M6.34 18H5.25A2.25 2.25 0 013 15.75V9.456c0-1.081.768-2.015 1.837-2.175a48.041 48.041 0 011.913-.247m10.5 0a48.536 48.536 0 00-10.5 0v2.756a2.25 2.25 0 002.25 2.25h6a2.25 2.25 0 002.25-2.25V9.229z" /></svg>
</a>
</label>
`;
});
$('#slip_bulk_action').removeClass('hidden');
} else {
slipsHtml = '<p class="text-slate-500 col-span-2 text-center py-4">ไม่พบข้อมูลสลิปเงินเดือน</p>';
$('#slip_bulk_action').addClass('hidden');
}
$('#slips_list').html(slipsHtml);
// Render Taxes
let taxesHtml = '';
if (response.data.taxes.length > 0) {
response.data.taxes.forEach(tax => {
let yearTh = parseInt(tax.year_no) > 2500 ? parseInt(tax.year_no) : parseInt(tax.year_no) + 543;
taxesHtml += `
<a href="<?= BASE_URL ?>/kiosk/print/tax/${currentNid}/${tax.year_no}" target="_blank" class="flex items-center justify-between p-4 bg-white border border-slate-200 rounded-2xl hover:border-orange-300 hover:shadow-md transition-all group">
<div>
<p class="font-semibold text-slate-800 text-lg">ใบ 50 ทวิ</p>
<p class="text-sm text-slate-500">ประจำปี ${yearTh}</p>
</div>
<div class="w-10 h-10 rounded-full bg-slate-50 flex items-center justify-center text-slate-400 group-hover:bg-orange-50 group-hover:text-orange-500 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" /></svg>
</div>
</a>
`;
});
} else {
taxesHtml = '<p class="text-slate-500 col-span-2 text-center py-4">ไม่พบข้อมูลใบเสียภาษี</p>';
}
$('#taxes_list').html(taxesHtml);
switchTab('slip');
$('#waiting_state').addClass('hidden');
$('#data_state').removeClass('hidden');
} else {
showAlert(response.message || 'ไม่พบข้อมูล');
$('#waiting_state').html(`
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1" stroke="currentColor" class="w-32 h-32 text-slate-300 mb-6"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" /></svg>
<p class="text-2xl font-medium text-slate-600">รอการระบุข้อมูล</p>
<p class="text-slate-500 mt-2">พิมพ์รหัสบัตรประชาชนเพื่อค้นหาเอกสาร</p>
`);
}
}, 'json').fail(function() {
showAlert('เกิดข้อผิดพลาดในการเชื่อมต่อเซิร์ฟเวอร์');
});
}
function printSelectedSlips() {
let selected = [];
$('.slip-checkbox:checked').each(function() {
selected.push($(this).val());
});
if (selected.length === 0) {
showAlert('กรุณาเลือกอย่างน้อย 1 รายการ');
return;
}
// Create a dynamic form to submit
let form = document.createElement('form');
form.method = 'POST';
form.action = '<?= BASE_URL ?>/kiosk/print/slipBulk';
form.target = '_blank';
let inputNid = document.createElement('input');
inputNid.type = 'hidden';
inputNid.name = 'national_id';
inputNid.value = currentNid;
form.appendChild(inputNid);
selected.forEach(val => {
let inputVal = document.createElement('input');
inputVal.type = 'hidden';
inputVal.name = 'periods[]';
inputVal.value = val;
form.appendChild(inputVal);
});
document.body.appendChild(form);
form.submit();
document.body.removeChild(form);
}
// Initialize display on load
updateDisplay();
</script>
</body>
</html>
@@ -0,0 +1,309 @@
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>สลิปเงินเดือน - Kiosk</title>
<!-- Google Fonts: Sarabun (Standard Thai Font) -->
<link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<!-- Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
sans: ['Sarabun', 'sans-serif'],
}
}
}
}
</script>
<style>
@media print {
body { background: white; margin: 0; padding: 0; }
.no-print { display: none !important; }
@page { size: A4; margin: 0; } /* Set A4 without margins */
* { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }
.slip-container {
padding: 1cm !important;
box-shadow: none !important;
max-width: 100% !important;
margin: 0 !important;
width: 210mm !important;
height: 297mm !important;
box-sizing: border-box !important;
overflow: hidden !important;
border: none !important;
}
.page-break { page-break-after: always; }
}
body { font-size: <?= htmlspecialchars($slipFontSize ?? '11pt') ?>; color: #1e293b; background-color: #f8fafc; font-family: 'Sarabun', sans-serif; }
.slip-container {
width: 210mm;
min-height: 297mm;
box-sizing: border-box;
margin: 2cm auto;
background: white;
padding: 1cm;
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
border-radius: 0.5rem;
border: 1px solid #e2e8f0;
}
table { width: 100%; border-collapse: collapse; margin-bottom: 0.75rem; }
th, td { border: 1px solid #cbd5e1; padding: 0.2rem 0.5rem; }
.bg-gray-header { background-color: #f1f5f9 !important; }
.bg-gray-light { background-color: #f8fafc !important; }
.bg-gray-dark { background-color: #e2e8f0 !important; }
.bg-white { background-color: #ffffff !important; }
.border-none { border: none !important; }
.border-l { border-left: 1px solid #cbd5e1 !important; }
.border-r { border-right: 1px solid #cbd5e1 !important; }
.border-t { border-top: 1px solid #cbd5e1 !important; }
.border-b { border-bottom: 1px solid #cbd5e1 !important; }
.text-center { text-align: center; }
.text-right { text-align: right; }
.font-bold { font-weight: 700; }
.text-sm { font-size: 0.875rem; }
.w-col-name { width: 35%; }
.w-col-amt { width: 15%; }
.table-rounded { border-radius: 0.5rem; overflow: hidden; border: 1px solid #cbd5e1; }
.table-rounded table { margin-bottom: 0; }
.table-rounded th, .table-rounded td { border-color: #e2e8f0; }
.table-rounded tr:last-child td { border-bottom: none; }
</style>
</head>
<body class="bg-gray-100 flex flex-col items-center pb-10">
<!-- Print Button (Hidden when printing) -->
<div class="mt-6 mb-2 print:hidden w-full max-w-[210mm] flex justify-end gap-4">
<button onclick="window.print()" class="flex items-center gap-2 bg-sky-500 hover:bg-sky-600 text-white px-6 py-3 rounded-xl font-semibold shadow-md transition-all active:scale-95">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M6.72 13.829c-.24.03-.48.062-.72.096m.72-.096a42.415 42.415 0 0110.56 0m-10.56 0L6.34 18m10.94-4.171c.24.03.48.062.72.096m-.72-.096L17.66 18m0 0l.229 2.523a1.125 1.125 0 01-1.12 1.227H7.231c-.662 0-1.18-.568-1.12-1.227L6.34 18m11.318 0h1.091A2.25 2.25 0 0021 15.75V9.456c0-1.081-.768-2.015-1.837-2.175a48.055 48.055 0 00-1.913-.247M6.34 18H5.25A2.25 2.25 0 013 15.75V9.456c0-1.081.768-2.015 1.837-2.175a48.041 48.041 0 011.913-.247m10.5 0a48.536 48.536 0 00-10.5 0v2.756a2.25 2.25 0 002.25 2.25h6a2.25 2.25 0 002.25-2.25V9.229z" /></svg>
พิมพ์สลิปที่เลือก
</button>
<button onclick="window.close()" class="flex items-center gap-2 bg-slate-200 hover:bg-slate-300 text-slate-700 px-6 py-3 rounded-xl font-semibold shadow-sm transition-all active:scale-95">
ปิดหน้าต่าง
</button>
</div>
<?php
$totalSlips = count($bulkData);
foreach ($bulkData as $index => $data):
$salary = $data['salary'];
$hrData = $data['hrData'] ?? [];
$bankProfile = $data['bankProfile'] ?? [];
$incomes = $data['incomes'];
$deductions = $data['deductions'];
$month = $data['month'];
$year = $data['year'];
$thaiMonths = [
'01' => 'มกราคม', '02' => 'กุมภาพันธ์', '03' => 'มีนาคม', '04' => 'เมษายน',
'05' => 'พฤษภาคม', '06' => 'มิถุนายน', '07' => 'กรกฎาคม', '08' => 'สิงหาคม',
'09' => 'กันยายน', '10' => 'ตุลาคม', '11' => 'พฤศจิกายน', '12' => 'ธันวาคม'
];
$thaiYear = $year + 543;
$monthName = $thaiMonths[$month] ?? '';
// Grouping by Fund Source
$cgdIncomes = [];
$hospIncomes = [];
foreach($incomes as $inc) {
if ($inc['amount'] > 0) {
if (($inc['fund_source'] ?? '') === 'CGD') $cgdIncomes[] = $inc;
else $hospIncomes[] = $inc;
}
}
$cgdDeductions = [];
$hospDeductions = [];
foreach($deductions as $ded) {
if ($ded['amount'] > 0) {
if (($ded['fund_source'] ?? '') === 'CGD') $cgdDeductions[] = $ded;
else $hospDeductions[] = $ded;
}
}
$cgdTotalIncome = array_sum(array_column($cgdIncomes, 'amount'));
$cgdTotalDeduction = array_sum(array_column($cgdDeductions, 'amount'));
$hospTotalIncome = array_sum(array_column($hospIncomes, 'amount'));
$hospTotalDeduction = array_sum(array_column($hospDeductions, 'amount'));
$netSalary = ($cgdTotalIncome + $hospTotalIncome) - ($cgdTotalDeduction + $hospTotalDeduction);
$pageBreakClass = ($index < $totalSlips - 1) ? 'page-break' : '';
?>
<div class="slip-container <?= $pageBreakClass ?>" style="position: relative;">
<!-- Header Info Table -->
<table>
<tr>
<td colspan="4" class="bg-gray-header p-6 text-center relative border-b-0 rounded-t-lg">
<img src="<?= BASE_URL ?>/public/img/logo.png" alt="Logo" class="h-20 absolute top-4 left-1/2 transform -translate-x-1/2" onerror="this.style.display='none'">
<h1 class="text-3xl font-bold mt-20 text-slate-800">โรงพยาบาลเกาะสมุย</h1>
</td>
</tr>
<tr>
<td class="bg-gray-light font-bold w-[20%]">ชื่อ - สกุล</td>
<td class="w-[30%]"><?= htmlspecialchars($hrData['HR_FNAME'] ?? $salary['first_name']) ?> <?= htmlspecialchars($hrData['HR_LNAME'] ?? $salary['last_name']) ?></td>
<td class="bg-gray-light font-bold w-[20%]">ตำแหน่ง</td>
<td class="w-[30%]"><?= htmlspecialchars($hrData['HR_POSITION_NAME'] ?? '-') ?></td>
</tr>
<tr>
<td class="bg-gray-light font-bold">กรม</td>
<td>สำนักงานปลัดกระทรวงสาธารณสุข</td>
<td class="bg-gray-light font-bold">ระดับ</td>
<td><?= htmlspecialchars($hrData['HR_LEVEL_NAME'] ?? 'ชำนาญการ') ?></td>
</tr>
<tr>
<td class="bg-gray-light font-bold">สำนัก/กอง/ศูนย์</td>
<td>โรงพยาบาลทั่วไปเกาะสมุย</td>
<td class="bg-gray-light font-bold">เลขที่ตำแหน่ง</td>
<td><?= htmlspecialchars($hrData['HR_POSITION_NUM'] ?? '-') ?></td>
</tr>
<tr>
<td class="bg-gray-light font-bold">รอบจ่าย</td>
<td>เดือน <?= $monthName ?> ปี <?= $thaiYear ?></td>
<td class="bg-gray-light font-bold">แผนก</td>
<td><?= htmlspecialchars($hrData['HR_DEPARTMENT_SUB_SUB_NAME'] ?? '-') ?></td>
</tr>
<tr>
<td class="bg-gray-light font-bold">โอนเงินเข้า</td>
<td class="w-[30%]"><?= htmlspecialchars($bankProfile['bank_name'] ?? 'ธนาคารกรุงไทย จำกัด(มหาชน)') ?></td>
<td class="bg-gray-light font-bold w-[20%]">เลขที่บัญชี</td>
<td class="w-[30%]"><?= htmlspecialchars($bankProfile['bank_account'] ?? '-') ?></td>
</tr>
</table>
<?php if (!empty($cgdIncomes) || !empty($cgdDeductions)): ?>
<!-- Section 1: กรมบัญชีกลาง -->
<table class="mb-4">
<tr>
<td colspan="4" class="bg-gray-dark font-bold text-lg p-3 text-slate-800 border-x-0 border-t-0">รายการจากกรมบัญชีกลาง</td>
</tr>
<tr class="bg-gray-light text-center font-bold">
<td class="w-col-name">รายรับ</td>
<td class="w-col-amt">จำนวน (บาท)</td>
<td class="w-col-name">รายจ่าย</td>
<td class="w-col-amt">จำนวน (บาท)</td>
</tr>
<?php
$maxCgdRows = max(count($cgdIncomes), count($cgdDeductions), 1);
for($i=0; $i<$maxCgdRows; $i++):
$inc = $cgdIncomes[$i] ?? null;
$ded = $cgdDeductions[$i] ?? null;
?>
<tr>
<td class="border-t-0 border-b-0 border-l border-r"><?= $inc ? htmlspecialchars($inc['name']) : '&nbsp;' ?></td>
<td class="border-t-0 border-b-0 border-l border-r text-right"><?= $inc ? number_format($inc['amount'], 2) : '' ?></td>
<td class="border-t-0 border-b-0 border-l border-r"><?= $ded ? htmlspecialchars($ded['name']) : '' ?></td>
<td class="border-t-0 border-b-0 border-l border-r text-right"><?= $ded ? number_format($ded['amount'], 2) : '' ?></td>
</tr>
<?php endfor; ?>
<tr>
<td class="border-t border-b border-l border-r font-bold text-center bg-gray-light text-slate-700">รวมรับ</td>
<td class="border-t border-b border-l border-r text-right font-medium"><?= number_format($cgdTotalIncome, 2) ?></td>
<td class="border-t border-b border-l border-r font-bold text-center bg-gray-light text-slate-700">รวมจ่าย</td>
<td class="border-t border-b border-l border-r text-right font-medium"><?= number_format($cgdTotalDeduction, 2) ?></td>
</tr>
<tr>
<td colspan="2" class="border-t border-b border-l border-r bg-white" style="border-right: none;"></td>
<td class="border-t border-b border-l border-r font-bold text-center bg-gray-dark text-slate-800">รับสุทธิจากกรมบัญชีกลาง</td>
<td class="border-t border-b border-l border-r text-right bg-white font-bold text-sky-700"><?= number_format($cgdTotalIncome - $cgdTotalDeduction, 2) ?></td>
</tr>
</table>
<?php endif; ?>
<?php if (!empty($hospIncomes) || !empty($hospDeductions)): ?>
<!-- Section 2: โรงพยาบาล -->
<table class="mb-4">
<tr>
<td colspan="4" class="bg-gray-dark font-bold text-lg p-3 text-slate-800 border-x-0 border-t-0">รายการจากโรงพยาบาล</td>
</tr>
<tr class="bg-gray-light text-center font-bold">
<td class="w-col-name">รายรับ</td>
<td class="w-col-amt">จำนวน (บาท)</td>
<td class="w-col-name">รายจ่าย</td>
<td class="w-col-amt">จำนวน (บาท)</td>
</tr>
<?php
$maxHospRows = max(count($hospIncomes), count($hospDeductions), 1);
for($i=0; $i<$maxHospRows; $i++):
$inc = $hospIncomes[$i] ?? null;
$ded = $hospDeductions[$i] ?? null;
?>
<tr>
<td class="border-t-0 border-b-0 border-l border-r"><?= $inc ? htmlspecialchars($inc['name']) : '&nbsp;' ?></td>
<td class="border-t-0 border-b-0 border-l border-r text-right"><?= $inc ? number_format($inc['amount'], 2) : '' ?></td>
<td class="border-t-0 border-b-0 border-l border-r"><?= $ded ? htmlspecialchars($ded['name']) : '' ?></td>
<td class="border-t-0 border-b-0 border-l border-r text-right"><?= $ded ? number_format($ded['amount'], 2) : '' ?></td>
</tr>
<?php endfor; ?>
<tr>
<td class="border-t border-b border-l border-r font-bold text-center bg-gray-light text-slate-700">รวมรับ</td>
<td class="border-t border-b border-l border-r text-right font-medium"><?= number_format($hospTotalIncome, 2) ?></td>
<td class="border-t border-b border-l border-r font-bold text-center bg-gray-light text-slate-700">รวมจ่าย</td>
<td class="border-t border-b border-l border-r text-right font-medium"><?= number_format($hospTotalDeduction, 2) ?></td>
</tr>
<tr>
<td colspan="2" class="border-t border-b border-l border-r bg-white" style="border-right: none;"></td>
<td class="border-t border-b border-l border-r font-bold text-center bg-gray-dark text-slate-800">รับสุทธิจากโรงพยาบาล</td>
<td class="border-t border-b border-l border-r text-right bg-white font-bold text-sky-700"><?= number_format($hospTotalIncome - $hospTotalDeduction, 2) ?></td>
</tr>
</table>
<?php endif; ?>
<!-- Grand Totals -->
<table class="mb-4">
<tr>
<td class="bg-gray-dark font-bold text-center text-xl p-4 w-[85%] text-slate-800 rounded-l-lg border-r-0">รวมรับสุทธิทั้งสิ้น</td>
<td class="text-right font-bold text-xl p-4 w-[15%] bg-white text-emerald-600 rounded-r-lg border-l-0"><?= number_format($netSalary, 2) ?></td>
</tr>
</table>
<!-- Signature -->
<div class="mt-16 flex justify-end pr-8">
<table class="text-center w-72 border-none">
<tr class="border-none">
<td class="text-right w-16 pr-2 border-none pb-1 align-bottom text-slate-700">ลงชื่อ</td>
<td class="border-b border-dotted border-slate-300 border-t-0 border-l-0 border-r-0 h-16 align-bottom pb-1 relative">
<?php
$sigPath = APP_ROOT . '/public/img/signature.png';
if (file_exists($sigPath)):
$modTime = filemtime($sigPath);
// signatureScale from Controller (default 100)
$scale = $signatureScale ?? 100;
$maxHeight = 3 * ($scale / 100) . 'rem'; // 3rem is roughly 48px (h-12)
// Signature position shift (default 0)
$posX = $signaturePosX ?? '0';
$posY = $signaturePosY ?? '0';
?>
<img src="<?= BASE_URL ?>/public/img/signature.png?t=<?= $modTime ?>" alt="Signature" class="mx-auto object-contain mix-blend-multiply absolute bottom-1 left-0 right-0" style="max-height: <?= $maxHeight ?>; transform: translate(<?= $posX ?>px, <?= $posY ?>px);" onerror="this.style.display='none'">
<?php endif; ?>
</td>
</tr>
<tr class="border-none">
<td class="border-none"></td>
<td class="pt-4 border-none"><?= htmlspecialchars($signatureName ?? '( นางณัฐฐิณี เรืองทอง )') ?></td>
</tr>
<tr class="border-none">
<td class="border-none"></td>
<td class="border-none"><?= htmlspecialchars($signaturePosition ?? 'นักวิชาการเงินและบัญชี') ?></td>
</tr>
</table>
</div>
</div>
<?php endforeach; ?>
</body>
</html>
@@ -0,0 +1,112 @@
</main>
<footer class="mt-auto py-6 px-6 lg:px-8 border-t border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 transition-colors">
<div class="flex flex-col md:flex-row justify-between items-center text-sm text-slate-500 dark:text-slate-400">
<div class="mb-2 md:mb-0">
&copy; <?= date('Y') + 543 ?> โรงพยาบาลเกาะสมุย (Samui Hospital). All rights reserved.
</div>
<div class="flex items-center space-x-4">
<span>ระบบจัดการข้อมูลเงินเดือนเจ้าหน้าที่</span>
<div class="mt-4 md:mt-0 flex items-center space-x-4">
<span class="text-sm font-medium text-slate-500 bg-white dark:bg-slate-800 px-3 py-1 rounded-full shadow-sm border border-slate-200 dark:border-slate-700">
v<?= htmlspecialchars((new \app\Models\SystemSettingModel())->getSetting('app_version', '1.0.0')) ?>
</span>
</div>
</div>
</div>
</footer>
</div>
</div>
<!-- SweetAlert2 -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Theme Toggle Logic
const themeToggle = document.getElementById('themeToggle');
const themeIcon = document.getElementById('themeIcon');
const html = document.documentElement;
function updateThemeIcon() {
if (html.classList.contains('dark')) {
themeIcon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" />';
} else {
themeIcon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z" />';
}
}
updateThemeIcon();
if (themeToggle) {
themeToggle.addEventListener('click', () => {
html.classList.toggle('dark');
if (html.classList.contains('dark')) {
localStorage.theme = 'dark';
} else {
localStorage.theme = 'light';
}
updateThemeIcon();
});
}
// Sidebar Toggle Logic for Mobile
const sidebarToggle = document.getElementById('sidebarToggle');
const sidebar = document.getElementById('sidebar');
const sidebarOverlay = document.getElementById('sidebarOverlay');
function toggleSidebar() {
sidebar.classList.toggle('-translate-x-full');
if (sidebar.classList.contains('-translate-x-full')) {
sidebarOverlay.classList.remove('opacity-100');
setTimeout(() => sidebarOverlay.classList.add('hidden'), 300);
} else {
sidebarOverlay.classList.remove('hidden');
setTimeout(() => sidebarOverlay.classList.add('opacity-100'), 10);
}
}
if (sidebarToggle) sidebarToggle.addEventListener('click', toggleSidebar);
if (sidebarOverlay) sidebarOverlay.addEventListener('click', toggleSidebar);
// SweetAlert2 Alerts
const Toast = Swal.mixin({
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 3000,
timerProgressBar: true,
didOpen: (toast) => {
toast.addEventListener('mouseenter', Swal.stopTimer)
toast.addEventListener('mouseleave', Swal.resumeTimer)
}
});
<?php if (isset($_SESSION['error'])): ?>
Toast.fire({
icon: 'error',
title: '<?= htmlspecialchars($_SESSION['error'], ENT_QUOTES, 'UTF-8') ?>'
});
<?php unset($_SESSION['error']); ?>
<?php endif; ?>
<?php if (isset($_SESSION['success'])): ?>
Toast.fire({
icon: 'success',
title: '<?= htmlspecialchars($_SESSION['success'], ENT_QUOTES, 'UTF-8') ?>'
});
<?php unset($_SESSION['success']); ?>
<?php endif; ?>
// PWA Service Worker Registration
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('<?= BASE_URL ?>/public/sw.js')
.then(registration => console.log('SW registered'))
.catch(err => console.log('SW registration failed:', err));
});
}
});
</script>
</body>
</html>
@@ -0,0 +1,164 @@
<!DOCTYPE html>
<html lang="th" class="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= htmlspecialchars($title ?? APP_NAME) ?></title>
<link rel="icon" type="image/png" href="<?= BASE_URL ?>/public/favicon.png">
<link rel="shortcut icon" type="image/x-icon" href="<?= BASE_URL ?>/public/favicon.ico">
<!-- Google Fonts: Inter -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<!-- Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'sans-serif'],
},
colors: {
primary: '#0ea5e9',
secondary: '#14b8a6',
dark: '#0f172a'
}
}
}
}
// Apply theme immediately to prevent flash
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
</script>
<!-- SweetAlert2 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css">
<!-- PWA -->
<link rel="manifest" href="<?= BASE_URL ?>/public/manifest.json">
<meta name="theme-color" content="#0ea5e9">
</head>
<body class="bg-slate-50 dark:bg-slate-900 text-slate-800 dark:text-slate-200 transition-colors duration-200 overflow-x-hidden">
<div class="flex h-screen overflow-hidden">
<!-- Sidebar -->
<aside id="sidebar" class="w-64 bg-white/70 dark:bg-slate-800/70 backdrop-blur-xl border-r border-slate-200 dark:border-slate-700 flex-shrink-0 transition-all duration-300 transform -translate-x-full lg:translate-x-0 fixed lg:relative z-40 h-full shadow-lg">
<div class="h-16 flex items-center justify-center border-b border-slate-200 dark:border-slate-700 px-4">
<div class="flex items-center text-primary dark:text-sky-400 font-bold text-xl">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-6 h-6 mr-2">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 21h19.5m-18-18v18m10.5-18v18m6-13.5V21M6.75 6.75h.75m-.75 3h.75m-.75 3h.75m3-6h.75m-.75 3h.75m-.75 3h.75M6.75 21v-3.375c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21M3 3h12m-.75 4.5H21m-3.75 3.75h.008v.008h-.008v-.008Zm0 3h.008v.008h-.008v-.008Zm0 3h.008v.008h-.008v-.008Z" />
</svg>
ระบบเงินเดือน
</div>
</div>
<div class="overflow-y-auto h-[calc(100vh-4rem)] p-4 space-y-6">
<div>
<a href="<?= BASE_URL ?>/dashboard" class="flex items-center px-4 py-2.5 rounded-xl font-medium transition-all <?= ($activeMenu ?? '') === 'dashboard' ? 'bg-sky-100 text-sky-700 dark:bg-sky-900/40 dark:text-sky-400' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800' ?>">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 mr-3"><path stroke-linecap="round" stroke-linejoin="round" d="M10.5 6a7.5 7.5 0 1 0 7.5 7.5h-7.5V6Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 10.5H21A7.5 7.5 0 0 0 13.5 3v7.5Z" /></svg>
แผงควบคุม
</a>
</div>
<div>
<p class="px-4 text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">ระบบหลัก</p>
<a href="<?= BASE_URL ?>/payroll" class="flex items-center px-4 py-2.5 rounded-xl font-medium transition-all <?= ($activeMenu ?? '') === 'payroll' ? 'bg-sky-100 text-sky-700 dark:bg-sky-900/40 dark:text-sky-400' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800' ?>">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 mr-3"><path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5" /></svg>
งวดเงินเดือน
</a>
<a href="<?= BASE_URL ?>/import" class="flex items-center px-4 py-2.5 rounded-xl font-medium transition-all <?= ($activeMenu ?? '') === 'import' ? 'bg-sky-100 text-sky-700 dark:bg-sky-900/40 dark:text-sky-400' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800' ?> mt-1">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 mr-3"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5" /></svg>
นำเข้าข้อมูล
</a>
<a href="<?= BASE_URL ?>/reports" class="flex items-center px-4 py-2.5 rounded-xl font-medium transition-all <?= ($activeMenu ?? '') === 'reports' ? 'bg-sky-100 text-sky-700 dark:bg-sky-900/40 dark:text-sky-400' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800' ?> mt-1">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 mr-3"><path stroke-linecap="round" stroke-linejoin="round" d="M6.72 13.829c-.24.03-.48.062-.72.096m.72-.096a42.415 42.415 0 0 1 10.56 0m-10.56 0L6.34 18m10.94-4.171c.24.03.48.062.72.096m-.72-.096L17.66 18m0 0 .229 2.523a1.125 1.125 0 0 1-1.12 1.227H7.231c-.662 0-1.18-.568-1.12-1.227L6.34 18m11.318 0h1.091A2.25 2.25 0 0 0 21 15.75V9.456c0-1.081-.768-2.015-1.837-2.175a48.055 48.055 0 0 0-1.913-.247M6.34 18H5.25A2.25 2.25 0 0 1 3 15.75V9.456c0-1.081.768-2.015 1.837-2.175a48.041 48.041 0 0 1 1.913-.247m10.5 0a48.536 48.536 0 0 0-10.5 0m10.5 0V3.375c0-.621-.504-1.125-1.125-1.125h-8.25c-.621 0-1.125.504-1.125 1.125v3.659M18 10.5h.008v.008H18V10.5Zm-3 0h.008v.008H15V10.5Z" /></svg>
พิมพ์สลิป/50 ทวิ
</a>
<a href="<?= BASE_URL ?>/reports/salary" class="flex items-center px-4 py-2.5 rounded-xl font-medium transition-all <?= ($activeMenu ?? '') === 'report_salary' ? 'bg-sky-100 text-sky-700 dark:bg-sky-900/40 dark:text-sky-400' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800' ?> mt-1">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 mr-3"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m5.231 13.481L15 17.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Zm3.75 11.625a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z" /></svg>
รายงานเงินเดือน
</a>
</div>
<div>
<p class="px-4 text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">การตั้งค่า</p>
<a href="<?= BASE_URL ?>/settings/income" class="flex items-center px-4 py-2.5 rounded-xl font-medium transition-all <?= ($activeMenu ?? '') === 'setting_income' ? 'bg-sky-100 text-sky-700 dark:bg-sky-900/40 dark:text-sky-400' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800' ?>">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 mr-3"><path stroke-linecap="round" stroke-linejoin="round" d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 0 1 1.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 0 1-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 0 1-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 0 1 .12-1.45l.773-.773a1.125 1.125 0 0 1 1.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /></svg>
รายการรายรับ
</a>
<a href="<?= BASE_URL ?>/settings/deduction" class="flex items-center px-4 py-2.5 rounded-xl font-medium transition-all <?= ($activeMenu ?? '') === 'setting_deduction' ? 'bg-sky-100 text-sky-700 dark:bg-sky-900/40 dark:text-sky-400' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800' ?> mt-1">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 mr-3"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 18.75a60.07 60.07 0 0 1 15.797 2.101c.727.198 1.453-.342 1.453-1.096V18.75M3.75 4.5v.75A.75.75 0 0 1 3 6h-.75m0 0v-.375c0-.621.504-1.125 1.125-1.125H20.25M2.25 6v9m18-10.5v.75c0 .414.336.75.75.75h.75m-1.5-1.5h.375c.621 0 1.125.504 1.125 1.125v9.75c0 .621-.504 1.125-1.125 1.125h-.375m1.5-1.5H21a.75.75 0 0 0-.75.75v.75m0 0H3.75m0 0h-.375a1.125 1.125 0 0 1-1.125-1.125V15m1.5 1.5v-.75A.75.75 0 0 0 3 15h-.75M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm3 0h.008v.008H18V10.5Zm-12 0h.008v.008H6V10.5Z" /></svg>
รายการรายจ่าย
</a>
<a href="<?= BASE_URL ?>/settings/officer-types" class="flex items-center px-4 py-2.5 rounded-xl font-medium transition-all <?= ($activeMenu ?? '') === 'setting_officer_types' ? 'bg-sky-100 text-sky-700 dark:bg-sky-900/40 dark:text-sky-400' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800' ?> mt-1">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 mr-3"><path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z" /></svg>
ประเภทเจ้าหน้าที่
</a>
<a href="<?= BASE_URL ?>/settings/system" class="flex items-center px-4 py-2.5 rounded-xl font-medium transition-all <?= ($activeMenu ?? '') === 'setting_system' ? 'bg-sky-100 text-sky-700 dark:bg-sky-900/40 dark:text-sky-400' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800' ?> mt-1">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 mr-3"><path stroke-linecap="round" stroke-linejoin="round" d="M10.5 6h9.75M10.5 6a1.5 1.5 0 1 1-3 0m3 0a1.5 1.5 0 1 0-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-9.75 0h9.75" /></svg>
ตั้งค่าระบบทั่วไป
</a>
<?php if(isset($_SESSION['role_id']) && $_SESSION['role_id'] == 1): ?>
<a href="<?= BASE_URL ?>/users" class="flex items-center px-4 py-2.5 rounded-xl font-medium transition-all <?= ($activeMenu ?? '') === 'users' ? 'bg-sky-100 text-sky-700 dark:bg-sky-900/40 dark:text-sky-400' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800' ?> mt-1">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 mr-3"><path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z" /></svg>
ผู้ใช้งานระบบ
</a>
<?php endif; ?>
<hr class="border-slate-200 dark:border-slate-700 my-2">
<a href="<?= BASE_URL ?>/manual" class="flex items-center px-4 py-2.5 rounded-xl font-medium transition-all <?= ($activeMenu ?? '') === 'manual' ? 'bg-sky-100 text-sky-700 dark:bg-sky-900/40 dark:text-sky-400' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800' ?> mt-1">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 mr-3"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 0 0 6 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 0 1 6 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 0 1 6-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0 0 18 18a8.967 8.967 0 0 0-6 2.292m0-14.25v14.25" /></svg>
คู่มือการใช้งาน
</a>
</div>
</div>
</aside>
<!-- Overlay for Mobile -->
<div id="sidebarOverlay" class="fixed inset-0 bg-slate-900/50 z-30 hidden lg:hidden backdrop-blur-sm transition-opacity opacity-0"></div>
<!-- Main Content -->
<div class="flex-1 flex flex-col h-screen overflow-hidden">
<!-- Navbar -->
<header class="h-16 bg-white/70 dark:bg-slate-800/70 backdrop-blur-xl border-b border-slate-200 dark:border-slate-700 flex items-center justify-between px-4 lg:px-8 z-20">
<div class="flex items-center">
<button id="sidebarToggle" class="lg:hidden text-slate-500 hover:text-primary focus:outline-none p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25H12" /></svg>
</button>
<h1 class="ml-4 text-xl font-bold text-slate-800 dark:text-slate-100 hidden sm:block"><?= htmlspecialchars($title ?? '') ?></h1>
</div>
<div class="flex items-center space-x-4">
<button id="themeToggle" class="text-slate-500 hover:text-primary transition-colors p-2 rounded-full hover:bg-slate-100 dark:hover:bg-slate-700">
<svg id="themeIcon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z" /></svg>
</button>
<div class="relative group">
<button class="flex items-center space-x-2 text-slate-700 dark:text-slate-200 focus:outline-none">
<div class="w-8 h-8 rounded-full bg-gradient-to-r from-primary to-secondary flex items-center justify-center text-white font-bold">
<?= mb_substr(htmlspecialchars($_SESSION['first_name'] ?? 'U'), 0, 1) ?>
</div>
<span class="font-medium hidden md:block"><?= htmlspecialchars(($_SESSION['first_name'] ?? 'Guest') . ' ' . ($_SESSION['last_name'] ?? '')) ?></span>
</button>
<!-- Dropdown Menu -->
<div class="absolute right-0 mt-2 w-48 bg-white dark:bg-slate-800 rounded-xl shadow-lg border border-slate-100 dark:border-slate-700 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 transform origin-top-right z-50">
<div class="p-2">
<a href="<?= BASE_URL ?>/profile" class="block px-4 py-2 text-sm text-slate-700 dark:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-lg transition-colors">ข้อมูลส่วนตัว</a>
<hr class="my-1 border-slate-200 dark:border-slate-700">
<a href="<?= BASE_URL ?>/logout" class="block px-4 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/30 rounded-lg transition-colors font-medium">ออกจากระบบ</a>
</div>
</div>
</div>
</div>
</header>
<!-- Page Content -->
<main class="flex-1 overflow-x-hidden overflow-y-auto bg-slate-50 dark:bg-slate-900 p-4 lg:p-8">
@@ -0,0 +1,232 @@
<?php include '../app/Views/layouts/header.php'; ?>
<div class="p-4 sm:ml-64 bg-slate-50 dark:bg-slate-900 min-h-screen pt-20 transition-colors duration-300">
<div class="max-w-5xl mx-auto">
<!-- Header -->
<div class="bg-gradient-to-r from-sky-500 to-indigo-600 rounded-2xl p-8 mb-8 text-white shadow-lg relative overflow-hidden">
<div class="relative z-10">
<h1 class="text-3xl font-bold mb-2">คู่มือการใช้งานระบบ</h1>
<p class="text-sky-100 text-lg">ระบบจัดการข้อมูลเงินเดือนเจ้าหน้าที่โรงพยาบาล</p>
</div>
<!-- Decorative circle -->
<div class="absolute top-0 right-0 -mr-16 -mt-16 w-64 h-64 rounded-full bg-white opacity-10"></div>
<div class="absolute bottom-0 right-16 -mb-16 w-32 h-32 rounded-full bg-white opacity-10"></div>
</div>
<!-- TOC & Content Grid -->
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
<!-- Sidebar TOC (Desktop) -->
<div class="hidden lg:block lg:col-span-1">
<div class="sticky top-24 bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-100 dark:border-slate-700 p-5">
<h3 class="font-bold text-slate-800 dark:text-white mb-4">สารบัญ</h3>
<ul class="space-y-3 text-sm font-medium text-slate-600 dark:text-slate-400">
<li><a href="#intro" class="hover:text-sky-500 transition-colors">1. แนะนำระบบ</a></li>
<li><a href="#dashboard" class="hover:text-sky-500 transition-colors">2. แดชบอร์ด (Dashboard)</a></li>
<li><a href="#payroll" class="hover:text-sky-500 transition-colors">3. ข้อมูลเงินเดือน</a></li>
<li><a href="#print" class="hover:text-sky-500 transition-colors">4. พิมพ์สลิปเงินเดือน</a></li>
<li><a href="#kiosk" class="hover:text-sky-500 transition-colors">5. ระบบบริการตนเอง (Kiosk)</a></li>
<li><a href="#users" class="hover:text-sky-500 transition-colors">6. ผู้ใช้งานระบบและประวัติ</a></li>
<li><a href="#settings" class="hover:text-sky-500 transition-colors">7. ตั้งค่าระบบ</a></li>
<li><a href="#profile" class="hover:text-sky-500 transition-colors">8. ข้อมูลส่วนตัว</a></li>
</ul>
</div>
</div>
<!-- Content Area -->
<div class="lg:col-span-3 space-y-8">
<!-- 1. Intro -->
<section id="intro" class="bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-100 dark:border-slate-700 p-6 md:p-8 scroll-mt-24">
<div class="flex items-center mb-4">
<div class="w-10 h-10 rounded-lg bg-sky-100 dark:bg-sky-900/50 flex items-center justify-center text-sky-500 mr-4">
<span class="font-bold text-lg">1</span>
</div>
<h2 class="text-xl font-bold text-slate-800 dark:text-white m-0">แนะนำระบบ</h2>
</div>
<div class="prose dark:prose-invert max-w-none text-slate-600 dark:text-slate-300">
<p>ระบบจัดการข้อมูลเงินเดือนเจ้าหน้าที่โรงพยาบาล ถูกออกแบบมาเพื่ออำนวยความสะดวกในการนำเข้าข้อมูลเงินเดือนจากไฟล์ Excel, คำนวณรายรับ-รายจ่าย, และพิมพ์สลิปเงินเดือนสำหรับเจ้าหน้าที่ โดยมีระบบจัดการสิทธิ์ผู้ใช้งานและบันทึกประวัติการใช้งานอย่างครบถ้วน</p>
</div>
</section>
<!-- 2. Dashboard -->
<section id="dashboard" class="bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-100 dark:border-slate-700 p-6 md:p-8 scroll-mt-24">
<div class="flex items-center mb-4">
<div class="w-10 h-10 rounded-lg bg-indigo-100 dark:bg-indigo-900/50 flex items-center justify-center text-indigo-500 mr-4">
<span class="font-bold text-lg">2</span>
</div>
<h2 class="text-xl font-bold text-slate-800 dark:text-white m-0">แดชบอร์ดภาพรวม (Dashboard)</h2>
</div>
<div class="space-y-4 text-slate-600 dark:text-slate-300">
<p>หน้าแรกของระบบ จะแสดงข้อมูลสรุปที่สำคัญ ได้แก่:</p>
<ul class="list-disc pl-5 space-y-2">
<li><strong>สรุปข้อมูลล่าสุด:</strong> แสดงงวดเดือน/ปีล่าสุดที่มีข้อมูลในระบบ พร้อมจำนวนพนักงานและยอดเงินเดือนสุทธิรวม</li>
<li><strong>กราฟภาพรวมรายรับ-รายจ่าย:</strong> แสดงกราฟแท่งคู่เปรียบเทียบรายรับและรายจ่ายในแต่ละเดือนของปี
<ul class="list-circle pl-5 mt-1 text-sm text-slate-500 dark:text-slate-400">
<li>สามารถเปลี่ยนปี พ.ศ. ที่ต้องการดูข้อมูลได้จากปุ่มเลือกมุมขวาบนของกราฟ</li>
<li>จำนวนปีย้อนหลังในตัวเลือก สามารถกำหนดได้ที่หน้า <a href="#settings" class="text-sky-500 hover:underline">ตั้งค่าระบบทั่วไป</a></li>
</ul>
</li>
</ul>
</div>
</section>
<!-- 3. Payroll -->
<section id="payroll" class="bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-100 dark:border-slate-700 p-6 md:p-8 scroll-mt-24">
<div class="flex items-center mb-4">
<div class="w-10 h-10 rounded-lg bg-emerald-100 dark:bg-emerald-900/50 flex items-center justify-center text-emerald-500 mr-4">
<span class="font-bold text-lg">3</span>
</div>
<h2 class="text-xl font-bold text-slate-800 dark:text-white m-0">การจัดการข้อมูลเงินเดือน</h2>
</div>
<div class="space-y-4 text-slate-600 dark:text-slate-300">
<p>เมนูหลักสำหรับการจัดการงวดเงินเดือน แบ่งเป็น 3 ส่วนหลักคือ การเตรียมข้อมูล, การนำเข้า, และการแก้ไข:</p>
<h4 class="font-semibold text-slate-800 dark:text-white mt-4">3.1 การสร้างและจัดการงวด</h4>
<ul class="list-disc pl-5 space-y-2">
<li><strong>สร้างงวดใหม่:</strong> กดปุ่ม "สร้างงวดเงินเดือน" เลือกเดือนและปีที่ต้องการ (ระบบป้องกันการสร้างงวดซ้ำ)</li>
<li><strong>ดึงข้อมูลจากเดือนก่อน:</strong> หากต้องการใช้ฐานข้อมูลเดิม กด "คัดลอกข้อมูลจากเดือนก่อนหน้า" เพื่อคัดลอกรายชื่อ ฐานเงินเดือน รายรับ และรายจ่ายทั้งหมดมายังเดือนปัจจุบัน เพื่อประหยัดเวลาพิมพ์ใหม่</li>
</ul>
<h4 class="font-semibold text-slate-800 dark:text-white mt-4">3.2 การนำเข้าข้อมูล (Excel / CSV / TXT)</h4>
<ul class="list-disc pl-5 space-y-2">
<li><strong>ดาวน์โหลด Template:</strong> ในหน้ารายละเอียดงวด กด "นำเข้าข้อมูล" คุณสามารถโหลดไฟล์แม่แบบ <code>.xlsx</code> เพื่อกรอกข้อมูล</li>
<li><strong>อัปโหลดไฟล์ Excel:</strong> อัปโหลดไฟล์ที่กรอกข้อมูลครบถ้วน โดยระบบจะตรวจสอบความถูกต้องของหัวคอลัมน์อัตโนมัติ
<br><span class="text-sm bg-amber-50 text-amber-700 p-1 rounded inline-block mt-1">⚠️ ข้อควรระวัง: หากชื่อคอลัมน์ใน Excel ไม่ตรงกับที่มีในระบบ (ตั้งค่า > หมวดหมู่รายรับ/รายจ่าย) ระบบจะไม่สามารถนำเข้าข้อมูลนั้นได้</span>
</li>
<li><strong>อัปโหลดไฟล์ของกรมบัญชีกลาง (.txt):</strong>
<ul class="list-circle pl-5 mt-1 text-sm text-slate-500 dark:text-slate-400">
<li>รองรับการนำเข้าไฟล์ Text (<code>.txt</code>) ที่ส่งออกมาจากระบบเงินเดือนของกรมบัญชีกลางได้โดยตรง</li>
<li>ระบบจะทำการอ่านค่าตำแหน่งตัวอักษร (Fixed width) และจัดสรรเข้าสู่รายรับ-รายจ่ายที่ตรงกันโดยอัตโนมัติ</li>
</ul>
</li>
</ul>
<h4 class="font-semibold text-slate-800 dark:text-white mt-4">3.3 การจัดการสถานะงวด (ประกาศใช้ / ล็อก)</h4>
<ul class="list-disc pl-5 space-y-2">
<li><strong>สถานะเริ่มต้น (Draft):</strong> ข้อมูลจะยังแก้ไขได้และไม่แสดงผลให้พนักงานทั่วไปเห็นผ่านตู้ Kiosk</li>
<li><strong>ประกาศใช้ (Publish):</strong> เมื่อตรวจสอบข้อมูลเสร็จสิ้นแล้ว ให้กดปุ่ม <strong>"ประกาศใช้"</strong> (สีเขียว) สถานะจะเปลี่ยนไปและข้อมูลงวดนี้จะไปแสดงบนตู้ Kiosk ทันที!</li>
<li><strong>ปลดล็อก (Unlock):</strong> หากประกาศใช้ไปแล้วแต่พบข้อผิดพลาด สามารถกด <strong>"ปลดล็อกเพื่อแก้ไข"</strong> (สีส้ม) ระบบจะซ่อนข้อมูลจากตู้ Kiosk ชั่วคราวเพื่อให้ Admin แก้ไข</li>
</ul>
<h4 class="font-semibold text-slate-800 dark:text-white mt-4">3.4 การแก้ไขและคำนวณภาษี</h4>
<ul class="list-disc pl-5 space-y-2">
<li><strong>ตรวจสอบข้อมูล:</strong> หลังจากนำเข้าหรือคัดลอกมาแล้ว สามารถดูยอดเงินเดือนสุทธิรวมของแผนกต่างๆ ได้ในหน้าจอรายละเอียดงวด</li>
<li><strong>เปิด/ปิดคำนำหน้าชื่อ:</strong> ในตารางข้อมูลสามารถติ๊กถูกที่ <strong>"แสดงคำนำหน้าชื่อ"</strong> เพื่อสลับการแสดงผลคำนำหน้าชื่อออกไป ทำให้ค้นหาหรือเรียงลำดับตามชื่อได้อย่างแม่นยำขึ้น</li>
<li><strong>แก้ไขรายบุคคล:</strong> กดปุ่ม <strong>"View"</strong> เพื่อดูรายละเอียดสลิปของคนนั้น หรือไอคอนรูป <svg xmlns="http://www.w3.org/2000/svg" class="inline w-4 h-4 text-sky-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" /></svg> เพื่อเข้าไปเพิ่ม/ลดรายรับรายจ่ายเฉพาะบุคคลนั้น</li>
<li><strong>คำนวณภาษีอัตโนมัติ:</strong> ในหน้าแก้ไข จะมีปุ่ม <span class="bg-amber-100 text-amber-700 px-2 py-0.5 rounded text-xs border border-amber-200">✨ คำนวณภาษีอัตโนมัติ</span>
<ul class="list-circle pl-5 mt-1 text-sm text-slate-500">
<li>ระบบจะทำการรวม "เงินได้สะสมทั้งปี" และนำไปหักลดหย่อนตามที่กฎหมายกำหนด จากนั้นจึงประเมินภาษีอัตราก้าวหน้าให้ทันที</li>
<li>สามารถแก้ไขตัวเลขภาษีที่คำนวณได้เอง หากต้องการปรับแต่งตัวเลข</li>
</ul>
</li>
</ul>
</div>
</section>
<!-- 4. Print -->
<section id="print" class="bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-100 dark:border-slate-700 p-6 md:p-8 scroll-mt-24">
<div class="flex items-center mb-4">
<div class="w-10 h-10 rounded-lg bg-orange-100 dark:bg-orange-900/50 flex items-center justify-center text-orange-500 mr-4">
<span class="font-bold text-lg">4</span>
</div>
<h2 class="text-xl font-bold text-slate-800 dark:text-white m-0">การพิมพ์สลิปเงินเดือน</h2>
</div>
<div class="space-y-4 text-slate-600 dark:text-slate-300">
<p>คุณสามารถพิมพ์สลิปได้จากเมนู <strong>"พิมพ์สลิป"</strong> โดยมีขั้นตอนดังนี้:</p>
<ol class="list-decimal pl-5 space-y-2">
<li>เลือกงวดเดือน/ปี และแผนกที่ต้องการพิมพ์</li>
<li>สามารถติ๊กเลือก <input type="checkbox" checked disabled class="rounded border-slate-300 mr-1"> เฉพาะบุคคล หรือเลือกทั้งหมดในแผนก</li>
<li>กดปุ่ม <strong>"พิมพ์สลิปที่เลือก"</strong></li>
<li>ระบบจะเปิดหน้าต่างใหม่ เป็นรูปแบบสลิป A4 ที่จัด Layout ไว้พอดีหน้ากระดาษ (1 หน้า A4 ต่อ 1 สลิป)
<br><span class="text-sm text-rose-500">*หมายเหตุ: อย่าลืมตั้งค่าในเบราว์เซอร์ให้ขนาดกระดาษเป็น A4 และปิดระยะขอบ (Margins = None) ตอนสั่งพิมพ์</span>
</li>
</ol>
</div>
</section>
<!-- 5. Kiosk -->
<section id="kiosk" class="bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-100 dark:border-slate-700 p-6 md:p-8 scroll-mt-24">
<div class="flex items-center mb-4">
<div class="w-10 h-10 rounded-lg bg-yellow-100 dark:bg-yellow-900/50 flex items-center justify-center text-yellow-600 mr-4">
<span class="font-bold text-lg">5</span>
</div>
<h2 class="text-xl font-bold text-slate-800 dark:text-white m-0">ระบบบริการตนเอง (Kiosk)</h2>
</div>
<div class="space-y-4 text-slate-600 dark:text-slate-300">
<p>ระบบสำหรับเจ้าหน้าที่เพื่อสั่งพิมพ์สลิปเงินเดือนและใบ 50 ทวิ ด้วยตนเองผ่านหน้าจอสัมผัส โดยไม่ต้อง Login เข้าระบบ:</p>
<ul class="list-disc pl-5 space-y-2">
<li><strong>การเข้าใช้งาน:</strong> เข้าผ่าน URL <code>/kiosk</code> จะพบกับหน้าจอรูปแบบ 3D รองรับการสัมผัส (Touch Screen) เต็มรูปแบบ</li>
<li><strong>การค้นหา:</strong> เจ้าหน้าที่เพียงกรอกเลขบัตรประชาชน 13 หลัก (ใส่หรือไม่ใส่ขีดคั่นก็ได้) ระบบจะดึงข้อมูลที่ "ประกาศใช้" แล้วเท่านั้นมาแสดง</li>
<li><strong>พิมพ์สลิปแบบกลุ่ม (Bulk Print):</strong> เจ้าหน้าที่สามารถเลือกเดือนที่ต้องการพิมพ์ได้มากกว่า 1 เดือน และสั่งพิมพ์รวดเดียว ระบบจะแบ่งหน้าสลิป 1 หน้าต่อ 1 เดือนให้พอดี A4</li>
<li><strong>ใบเสียภาษี (50 ทวิ):</strong> สามารถเลือกปี พ.ศ. เพื่อพิมพ์ใบสรุปภาษีเงินได้ประจำปี เพื่อนำไปยื่นภาษีได้ทันที</li>
<li><strong>ประวัติการใช้งาน:</strong> ทุกการสั่งพิมพ์จากตู้ Kiosk จะถูกบันทึก Log เก็บไว้เพื่อให้ Admin สามารถตรวจสอบย้อนหลังได้</li>
</ul>
</div>
</section>
<!-- 6. Users -->
<section id="users" class="bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-100 dark:border-slate-700 p-6 md:p-8 scroll-mt-24">
<div class="flex items-center mb-4">
<div class="w-10 h-10 rounded-lg bg-purple-100 dark:bg-purple-900/50 flex items-center justify-center text-purple-500 mr-4">
<span class="font-bold text-lg">6</span>
</div>
<h2 class="text-xl font-bold text-slate-800 dark:text-white m-0">ผู้ใช้งานระบบและประวัติ</h2>
</div>
<div class="space-y-4 text-slate-600 dark:text-slate-300">
<p>เมนูนี้สำหรับผู้ดูแลระบบ (Admin) เท่านั้น แบ่งเป็น 2 ส่วน:</p>
<ul class="list-disc pl-5 space-y-2">
<li><strong>จัดการผู้ใช้งาน:</strong>
<ul class="list-circle pl-5 mt-1">
<li>เพิ่มผู้ใช้ใหม่ โดยค้นหาชื่อจากฐานข้อมูลเจ้าหน้าที่ (HR) อัตโนมัติ</li>
<li>เปิด/ปิดสิทธิ์การใช้งาน (Deactivate) ด้วยสวิตช์ สีเขียว/แดง</li>
<li>แก้ไขข้อมูลส่วนตัวและรหัสผ่านของผู้ใช้ได้</li>
</ul>
</li>
<li><strong>ประวัติการใช้งาน (Logs):</strong> บันทึกกิจกรรมทั้งหมดที่เกิดขึ้นในระบบ เช่น การเข้าสู่ระบบ การแก้ข้อมูล หรือปิดสิทธิ์ พร้อมระบบ Filter ค้นหาตามช่วงวันที่และประเภท</li>
</ul>
</div>
</section>
<!-- 7. Settings -->
<section id="settings" class="bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-100 dark:border-slate-700 p-6 md:p-8 scroll-mt-24">
<div class="flex items-center mb-4">
<div class="w-10 h-10 rounded-lg bg-slate-200 dark:bg-slate-700 flex items-center justify-center text-slate-600 dark:text-slate-300 mr-4">
<span class="font-bold text-lg">7</span>
</div>
<h2 class="text-xl font-bold text-slate-800 dark:text-white m-0">ตั้งค่าระบบ</h2>
</div>
<div class="space-y-4 text-slate-600 dark:text-slate-300">
<p>ใช้สำหรับปรับแต่งการทำงานของระบบให้ตรงตามต้องการ:</p>
<ul class="list-disc pl-5 space-y-2">
<li><strong>ตั้งค่าระบบทั่วไป:</strong> กำหนดข้อความใต้ลายเซ็น, เลขประจำตัวผู้เสียภาษีหน่วยงาน, จำนวนปีย้อนหลังในกราฟ
<br><span class="text-sm bg-sky-50 dark:bg-sky-900/30 text-sky-600 dark:text-sky-400 p-1 rounded inline-block mt-1">✨ พิเศษ: สามารถปรับขนาดลายเซ็น (Scale %) และขยับตำแหน่ง ซ้าย-ขวา, บน-ลง ได้จากที่นี่ทันที</span>
</li>
<li><strong>ตั้งค่าประเภทรายรับ:</strong> เพิ่ม/ลด/เปิด-ปิด รายการรายรับต่างๆ และกำหนดว่ารายรับนี้ต้องนำไปคำนวณภาษีด้วยหรือไม่</li>
<li><strong>ตั้งค่าประเภทรายจ่าย:</strong> เพิ่ม/ลด/เปิด-ปิด รายการหักเงิน และกำหนดหมวดหมู่ให้สอดคล้อง</li>
</ul>
</div>
</section>
<!-- 8. Profile -->
<section id="profile" class="bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-100 dark:border-slate-700 p-6 md:p-8 scroll-mt-24">
<div class="flex items-center mb-4">
<div class="w-10 h-10 rounded-lg bg-pink-100 dark:bg-pink-900/50 flex items-center justify-center text-pink-500 mr-4">
<span class="font-bold text-lg">8</span>
</div>
<h2 class="text-xl font-bold text-slate-800 dark:text-white m-0">ข้อมูลส่วนตัว</h2>
</div>
<div class="space-y-4 text-slate-600 dark:text-slate-300">
<p>มุมขวาบนของหน้าจอ (Topbar) จะแสดงชื่อ-สกุลของคุณ สามารถคลิกเพื่อเข้าสู่หน้า <strong>"ข้อมูลส่วนตัว"</strong></p>
<ul class="list-disc pl-5 space-y-2">
<li>ในหน้านี้ คุณสามารถดูรายละเอียด Username และระดับสิทธิ์ของตนเอง</li>
<li>สามารถทำการ <strong>"เปลี่ยนรหัสผ่าน"</strong> ได้ด้วยตนเอง โดยต้องกรอกรหัสผ่านเดิมให้ถูกต้องเพื่อยืนยันความปลอดภัย</li>
</ul>
</div>
</section>
</div>
</div>
</div>
</div>
<?php include '../app/Views/layouts/footer.php'; ?>
@@ -0,0 +1,413 @@
<?php require_once APP_ROOT . '/app/Views/layouts/header.php'; ?>
<?php
$thaiMonths = [
1 => 'มกราคม', 2 => 'กุมภาพันธ์', 3 => 'มีนาคม', 4 => 'เมษายน',
5 => 'พฤษภาคม', 6 => 'มิถุนายน', 7 => 'กรกฎาคม', 8 => 'สิงหาคม',
9 => 'กันยายน', 10 => 'ตุลาคม', 11 => 'พฤศจิกายน', 12 => 'ธันวาคม'
];
$yearBe = $period['year_no'] > 2500 ? $period['year_no'] : $period['year_no'] + 543;
$periodName = $thaiMonths[$period['month_no']] . ' ' . $yearBe;
$isLocked = ($period['status'] === 'Locked' || $period['status'] === 'Published');
?>
<div class="mb-6">
<div class="flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400 mb-2">
<a href="<?= BASE_URL ?>/payroll" class="hover:text-sky-500 transition-colors">จัดการงวดเงินเดือน</a>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-3 h-3"><path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" /></svg>
<span class="text-slate-800 dark:text-slate-200 font-medium">รายละเอียดงวด <?= $periodName ?></span>
</div>
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h2 class="text-2xl font-bold text-slate-800 dark:text-white">รายละเอียดงวดเงินเดือน</h2>
<p class="text-slate-500 dark:text-slate-400 text-sm mt-1">ประจำเดือน <?= $periodName ?> (สถานะ: <?= $period['status'] ?>)</p>
</div>
<?php if (!$isLocked): ?>
<div class="flex flex-wrap gap-2">
<form action="<?= BASE_URL ?>/payroll/updateStatus" method="POST" class="inline" onsubmit="return confirm('ยืนยันการล็อกและประกาศใช้ข้อมูล? (เมื่อทำแล้วจะไม่สามารถแก้ไขได้อีกและจะแสดงผลในตู้ Kiosk ทันที)');">
<input type="hidden" name="id" value="<?= $period['id'] ?>">
<input type="hidden" name="status" value="Published">
<button type="submit" class="inline-flex items-center justify-center px-4 py-2.5 bg-emerald-500 hover:bg-emerald-600 text-white text-sm font-medium rounded-xl transition-colors shadow-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" /></svg>
ประกาศใช้ (Lock)
</button>
</form>
<button type="button" onclick="confirmCopyPreviousMonth()" class="inline-flex items-center justify-center px-4 py-2.5 bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-600 text-sm font-medium rounded-xl transition-colors shadow-sm">
<svg class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75" />
</svg>
คัดลอกจากเดือนก่อน
</button>
<button onclick="document.getElementById('addEmployeeModal').classList.remove('hidden')" class="inline-flex items-center justify-center px-4 py-2.5 bg-sky-500 hover:bg-sky-600 text-white text-sm font-medium rounded-xl transition-colors shadow-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /></svg>
เพิ่มเจ้าหน้าที่
</button>
</div>
<?php else: ?>
<div class="flex flex-wrap gap-2">
<form action="<?= BASE_URL ?>/payroll/updateStatus" method="POST" class="inline" onsubmit="return confirm('ยืนยันการปลดล็อกข้อมูล? (เมื่อปลดล็อกแล้วข้อมูลจะไม่แสดงที่ตู้ Kiosk จนกว่าจะประกาศใช้อีกครั้ง)');">
<input type="hidden" name="id" value="<?= $period['id'] ?>">
<input type="hidden" name="status" value="Draft">
<button type="submit" class="inline-flex items-center justify-center px-4 py-2.5 bg-amber-500 hover:bg-amber-600 text-white text-sm font-medium rounded-xl transition-colors shadow-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 10.5V6.75a4.5 4.5 0 119 0v3.75M3.75 21.75h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H3.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" /></svg>
ปลดล็อกเพื่อแก้ไข
</button>
</form>
</div>
<?php endif; ?>
</div>
</div>
<!-- Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div class="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-sm border border-slate-100 dark:border-slate-700">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-slate-500 dark:text-slate-400">จำนวนพนักงาน</p>
<p class="text-2xl font-bold text-slate-800 dark:text-white mt-2"><?= number_format($stats['total_emp'] ?? 0) ?> <span class="text-sm font-normal text-slate-500">คน</span></p>
</div>
<div class="p-3 bg-sky-50 dark:bg-sky-900/30 rounded-xl">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-sky-500"><path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z" /></svg>
</div>
</div>
</div>
<div class="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-sm border border-slate-100 dark:border-slate-700">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-slate-500 dark:text-slate-400">ยอดเงินสุทธิรวม</p>
<p class="text-2xl font-bold text-slate-800 dark:text-white mt-2"><?= number_format($stats['total_net'] ?? 0, 2) ?> <span class="text-sm font-normal text-slate-500">บาท</span></p>
</div>
<div class="p-3 bg-emerald-50 dark:bg-emerald-900/30 rounded-xl">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-emerald-500"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m-3-2.818.879.659c1.171.879 3.07.879 4.242 0 1.172-.879 1.172-2.303 0-3.182C13.536 12.219 12.768 12 12 12c-.725 0-1.45-.22-2.003-.659-1.106-.879-1.106-2.303 0-3.182s2.9-.879 4.006 0l.415.33M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" /></svg>
</div>
</div>
</div>
</div>
<!-- DataTables CSS for Tailwind -->
<link rel="stylesheet" href="https://cdn.datatables.net/2.0.8/css/dataTables.tailwindcss.css">
<style>
/* DataTables Dark Mode Overrides */
.dark .dt-container { color: #cbd5e1; }
.dark .dt-length select, .dark .dt-search input {
background-color: #1e293b; border-color: #334155; color: #f8fafc; border-radius: 0.5rem; padding: 0.25rem 0.5rem;
}
.dark .dt-info, .dark .dt-paging-button { color: #94a3b8 !important; }
.dark .dt-paging-button.current { background: #38bdf8 !important; color: #0f172a !important; border-color: #38bdf8 !important; }
.dark .dt-paging-button:hover { background: #334155 !important; color: #f8fafc !important; }
table.dataTable.no-footer { border-bottom: 1px solid #334155 !important; }
</style>
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-100 dark:border-slate-700 overflow-hidden p-6">
<div class="flex justify-end mb-4">
<label class="inline-flex items-center cursor-pointer">
<input type="checkbox" id="togglePrefix" class="w-4 h-4 text-indigo-600 bg-slate-100 border-slate-300 rounded focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:ring-offset-slate-800 focus:ring-2 dark:bg-slate-700 dark:border-slate-600">
<span class="ml-2 text-sm font-medium text-slate-700 dark:text-slate-300">แสดงคำนำหน้าชื่อ</span>
</label>
</div>
<div class="overflow-x-hidden">
<table id="employeeTable" class="w-full text-left text-sm text-slate-600 dark:text-slate-300 display">
<thead class="bg-slate-50 dark:bg-slate-900/50 text-slate-500 dark:text-slate-400 text-xs uppercase font-semibold">
<tr>
<th class="px-6 py-4">ชื่อ - นามสกุล</th>
<th class="px-6 py-4">ตำแหน่ง / แผนก</th>
<th class="px-6 py-4 text-right text-emerald-600">รายรับรวม</th>
<th class="px-6 py-4 text-right text-red-600">รายจ่ายรวม</th>
<th class="px-6 py-4 text-right text-sky-600">ยอดสุทธิ</th>
<th class="px-6 py-4 text-right">จัดการ</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 dark:divide-slate-700">
<?php if (empty($employees)): ?>
<tr>
<td colspan="6" class="px-6 py-8 text-center text-slate-500 dark:text-slate-400">
ยังไม่มีพนักงานในงวดนี้ <?php if (!$isLocked): ?><button onclick="document.getElementById('addEmployeeModal').classList.remove('hidden')" class="text-sky-500 hover:underline">เพิ่มเลย</button><?php endif; ?>
</td>
</tr>
<?php else: ?>
<?php foreach ($employees as $emp): ?>
<tr class="hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors">
<td class="px-6 py-4" data-sort="<?= htmlspecialchars(preg_replace('/^(นาย|นางสาว|นาง|ว่าที่ร้อยตรี|น\.ส\.|นพ\.|พญ\.|ดร\.)\s*/u', '', trim($emp['first_name']))) ?>">
<div class="flex items-center gap-3">
<div class="h-10 w-10 flex-shrink-0">
<img class="h-10 w-10 rounded-full object-cover bg-slate-100 border border-slate-200 dark:border-slate-600" src="<?= BASE_URL ?>/employee/image/<?= htmlspecialchars($emp['national_id']) ?>" alt="<?= htmlspecialchars($emp['first_name']) ?>" onerror="this.src='data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 24 24\' fill=\'%2394a3b8\'><path fill-rule=\'evenodd\' d=\'M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z\' clip-rule=\'evenodd\'/></svg>'">
</div>
<div>
<?php
$rawName = trim($emp['first_name']);
$prefix = '';
$bodyName = $rawName;
if (preg_match('/^(นาย|นางสาว|นาง|ว่าที่ร้อยตรี|น\.ส\.|นพ\.|พญ\.|ดร\.)\s*(.*)$/u', $rawName, $matches)) {
$prefix = $matches[1];
$bodyName = $matches[2];
}
?>
<div class="font-medium text-slate-900 dark:text-white"><span class="name-prefix hidden"><?= htmlspecialchars($prefix) ?></span><?= htmlspecialchars($bodyName . ' ' . $emp['last_name']) ?></div>
<div class="text-xs text-slate-500 mt-0.5">ID: <?= htmlspecialchars($emp['national_id']) ?></div>
</div>
</div>
</td>
<td class="px-6 py-4">
<div class="text-slate-800 dark:text-slate-300"><?= htmlspecialchars($emp['position'] ?: '-') ?></div>
<div class="text-xs text-slate-500 mt-1"><?= htmlspecialchars($emp['department'] ?: '-') ?></div>
</td>
<td class="px-6 py-4 text-right text-emerald-600 font-medium">
<?= number_format($emp['total_income'], 2) ?>
</td>
<td class="px-6 py-4 text-right text-red-500 font-medium">
<?= number_format($emp['total_deduction'], 2) ?>
</td>
<td class="px-6 py-4 text-right text-sky-600 font-bold">
<?= number_format($emp['net_salary'], 2) ?>
</td>
<td class="px-6 py-4 text-right whitespace-nowrap">
<a href="<?= BASE_URL ?>/reports/slip/<?= htmlspecialchars($emp['national_id']) ?>/<?= sprintf('%02d', $period['month_no']) ?>/<?= $period['year_no'] ?>" target="_blank" class="inline-flex items-center justify-center p-1.5 text-slate-400 hover:text-indigo-500 hover:bg-indigo-50 dark:hover:bg-indigo-900/30 rounded-lg transition-colors mr-1" title="ดูสลิปเงินเดือน">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"><path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /></svg>
</a>
<?php if (!$isLocked): ?>
<a href="<?= BASE_URL ?>/payroll/employee/edit/<?= $emp['id'] ?>" class="inline-flex items-center justify-center p-1.5 text-slate-400 hover:text-sky-500 hover:bg-sky-50 dark:hover:bg-sky-900/30 rounded-lg transition-colors mr-1">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"><path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" /></svg>
</a>
<button type="button" onclick="confirmDeleteEmployee(<?= $emp['id'] ?>, '<?= htmlspecialchars($emp['first_name'] . ' ' . $emp['last_name']) ?>')" class="inline-flex items-center justify-center p-1.5 text-slate-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/30 rounded-lg transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"><path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" /></svg>
</button>
<?php else: ?>
<span class="text-xs text-slate-400">ล็อกแล้ว</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php if (!$isLocked): ?>
<!-- Add Employee Modal -->
<div id="addEmployeeModal" class="hidden fixed inset-0 z-50 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="fixed inset-0 bg-slate-900/50 backdrop-blur-sm transition-opacity" aria-hidden="true" onclick="document.getElementById('addEmployeeModal').classList.add('hidden')"></div>
<div class="flex min-h-screen items-center justify-center p-4 text-center sm:p-0">
<div class="relative transform overflow-hidden rounded-2xl bg-white dark:bg-slate-800 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg border border-slate-100 dark:border-slate-700">
<form action="<?= BASE_URL ?>/payroll/employee/add" method="POST">
<input type="hidden" name="salary_month_id" value="<?= $period['id'] ?>">
<div class="bg-white dark:bg-slate-800 px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div class="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-sky-100 dark:bg-sky-900/50 sm:mx-0 sm:h-10 sm:w-10">
<svg class="h-6 w-6 text-sky-600 dark:text-sky-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M18 7.5v3m0 0v3m0-3h3m-3 0h-3m-2.25-4.125a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0ZM3 19.235v-.11a6.375 6.375 0 0 1 12.75 0v.109A12.318 12.318 0 0 1 9.374 21c-2.331 0-4.512-.647-6.374-1.766Z" /></svg>
</div>
<div class="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left w-full">
<h3 class="text-lg font-semibold leading-6 text-slate-900 dark:text-white" id="modal-title">เพิ่มพนักงานในงวดนี้</h3>
<div class="mt-6 space-y-4">
<div class="mb-4 relative">
<label class="block text-sm font-medium leading-6 text-slate-700 dark:text-slate-300 mb-2">ค้นหาเจ้าหน้าที่จากระบบ HR</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-slate-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clip-rule="evenodd" />
</svg>
</div>
<input type="text" id="empSearch" placeholder="พิมพ์ชื่อ, นามสกุล เพื่อค้นหา..."
class="block w-full rounded-xl border-0 py-2.5 pl-10 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 placeholder:text-slate-400 focus:ring-2 focus:ring-inset focus:ring-sky-500 sm:text-sm sm:leading-6 bg-slate-50 dark:bg-slate-900">
</div>
<ul id="empSearchResults" class="absolute z-10 w-full bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl mt-1 max-h-60 overflow-y-auto hidden shadow-lg overflow-hidden"></ul>
</div>
<div>
<label for="national_id" class="block text-sm font-medium leading-6 text-slate-700 dark:text-slate-300 mb-2">เลขบัตรประชาชน 13 หลัก</label>
<input type="text" id="national_id" name="national_id" required pattern="[0-9]{13}" maxlength="13" placeholder="ระบุเลขบัตรประชาชน หรือเลือกจากการค้นหาด้านบน" class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 placeholder:text-slate-400 focus:ring-2 focus:ring-inset focus:ring-sky-500 sm:text-sm sm:leading-6 bg-slate-50 dark:bg-slate-900">
</div>
<div class="rounded-xl bg-sky-50 dark:bg-sky-900/30 p-4 mt-6">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-sky-400" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M19 10.5a8.5 8.5 0 1 1-17 0 8.5 8.5 0 0 1 17 0ZM8.25 9.75A.75.75 0 0 1 9 9h.253a1.75 1.75 0 0 1 1.709 2.13l-.46 2.066a.25.25 0 0 0 .245.304H11a.75.75 0 0 1 0 1.5h-.253a1.75 1.75 0 0 1-1.709-2.13l.46-2.066a.25.25 0 0 0-.245-.304H9a.75.75 0 0 1-.75-.75ZM10 7a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z" clip-rule="evenodd" /></svg>
</div>
<div class="ml-3">
<p class="text-sm text-sky-700 dark:text-sky-300 m-0">ระบบจะดึงชื่อ-นามสกุลจากระบบ HR (hosoffice_2566) อัตโนมัติ</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="bg-slate-50 dark:bg-slate-700/50 px-4 py-4 sm:flex sm:flex-row-reverse sm:px-6">
<button type="submit" class="inline-flex w-full justify-center rounded-xl bg-sky-500 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-sky-600 sm:ml-3 sm:w-auto transition-colors">ดึงข้อมูลและเพิ่ม</button>
<button type="button" onclick="document.getElementById('addEmployeeModal').classList.add('hidden')" class="mt-3 inline-flex w-full justify-center rounded-xl bg-white dark:bg-slate-800 px-4 py-2.5 text-sm font-semibold text-slate-900 dark:text-slate-300 shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 hover:bg-slate-50 dark:hover:bg-slate-700 sm:mt-0 sm:w-auto transition-colors">ยกเลิก</button>
</div>
</form>
</div>
</div>
</div>
<!-- Form for Delete Employee -->
<form id="deleteEmployeeForm" action="<?= BASE_URL ?>/payroll/employee/delete" method="POST" class="hidden">
<input type="hidden" name="id" id="deleteEmpId" value="">
<input type="hidden" name="salary_month_id" value="<?= $period['id'] ?>">
</form>
<!-- Form for Copy Previous Month -->
<form id="copyPreviousMonthForm" action="<?= BASE_URL ?>/payroll/copy-previous" method="POST" class="hidden">
<input type="hidden" name="salary_month_id" value="<?= $period['id'] ?>">
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
const searchInput = document.getElementById('empSearch');
const searchResults = document.getElementById('empSearchResults');
const nationalIdInput = document.getElementById('national_id');
let debounceTimer;
searchInput.addEventListener('input', function() {
const keyword = this.value.trim();
clearTimeout(debounceTimer);
if (keyword.length < 2) {
searchResults.classList.add('hidden');
searchResults.innerHTML = '';
return;
}
debounceTimer = setTimeout(function() {
fetch('<?= BASE_URL ?>/settings/system/search-officer?q=' + encodeURIComponent(keyword))
.then(response => {
if (!response.ok) throw new Error('Network error ' + response.status);
return response.text();
})
.then(text => {
let res;
try {
res = JSON.parse(text);
} catch (e) {
console.error('Invalid JSON from server:', text);
return;
}
searchResults.innerHTML = '';
if (res.status === 'success' && res.data && res.data.length > 0) {
res.data.forEach(item => {
const li = document.createElement('li');
li.className = 'px-4 py-3 hover:bg-sky-50 dark:hover:bg-slate-700 cursor-pointer text-sm text-slate-700 dark:text-slate-300 border-b border-slate-100 dark:border-slate-700 last:border-0';
li.innerHTML = `
<div class="font-medium">${item.name}</div>
<div class="text-xs text-slate-500 mt-1">${item.position}</div>
`;
li.addEventListener('click', function() {
nationalIdInput.value = item.id;
searchInput.value = item.name;
searchResults.classList.add('hidden');
// Highlight the input briefly to show it was populated
nationalIdInput.classList.add('ring-sky-500', 'ring-2');
setTimeout(() => {
nationalIdInput.classList.remove('ring-sky-500', 'ring-2');
}, 500);
});
searchResults.appendChild(li);
});
searchResults.classList.remove('hidden');
} else {
const li = document.createElement('li');
li.className = 'px-4 py-3 text-sm text-slate-500 dark:text-slate-400 italic text-center';
li.textContent = res.message || 'ไม่พบข้อมูล';
searchResults.appendChild(li);
searchResults.classList.remove('hidden');
}
})
.catch(error => {
console.error('Error searching:', error);
});
}, 400);
});
// Hide results when clicking outside
document.addEventListener('click', function(e) {
if (!searchInput.contains(e.target) && !searchResults.contains(e.target)) {
searchResults.classList.add('hidden');
}
});
});
function confirmDeleteEmployee(id, name) {
Swal.fire({
title: 'ยืนยันการลบ?',
html: `คุณต้องการลบ <b>${name}</b> ออกจากงวดเงินเดือนนี้ใช่หรือไม่?<br><span class="text-sm text-red-500">ยอดเงินและรายการรับจ่ายทั้งหมดของคนนี้จะถูกลบไปด้วย</span>`,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#ef4444',
cancelButtonColor: '#64748b',
confirmButtonText: 'ใช่, ลบเลย',
cancelButtonText: 'ยกเลิก'
}).then((result) => {
if (result.isConfirmed) {
document.getElementById('deleteEmpId').value = id;
document.getElementById('deleteEmployeeForm').submit();
}
});
}
function confirmCopyPreviousMonth() {
Swal.fire({
title: 'คัดลอกข้อมูลจากเดือนก่อน?',
html: `ระบบจะคัดลอกรายชื่อพนักงาน พร้อมรายการรายได้และรายจ่ายทั้งหมด จากงวดเงินเดือนเดือนที่แล้ว มายังงวดปัจจุบัน<br><br><span class="text-sm text-sky-600">พนักงานที่มีชื่ออยู่ในงวดนี้แล้วจะถูกข้ามไป ไม่คัดลอกซ้ำ</span>`,
icon: 'info',
showCancelButton: true,
confirmButtonColor: '#4f46e5',
cancelButtonColor: '#64748b',
confirmButtonText: 'ใช่, เริ่มคัดลอก',
cancelButtonText: 'ยกเลิก'
}).then((result) => {
if (result.isConfirmed) {
document.getElementById('copyPreviousMonthForm').submit();
}
});
}
</script>
<?php endif; ?>
<!-- jQuery and DataTables JS -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.datatables.net/2.0.8/js/dataTables.js"></script>
<script src="https://cdn.datatables.net/2.0.8/js/dataTables.tailwindcss.js"></script>
<script>
$(document).ready(function() {
$('#employeeTable').DataTable({
"language": {
"sProcessing": "กำลังดำเนินการ...",
"sLengthMenu": "แสดง _MENU_ เรคคอร์ด",
"sZeroRecords": "ไม่พบข้อมูล",
"sInfo": "แสดง _START_ ถึง _END_ จาก _TOTAL_ เรคคอร์ด",
"sInfoEmpty": "แสดง 0 ถึง 0 จาก 0 เรคคอร์ด",
"sInfoFiltered": "(กรองข้อมูล _MAX_ ทุกเรคคอร์ด)",
"sSearch": "ค้นหา:",
"oPaginate": {
"sFirst": "แรกสุด",
"sPrevious": "ก่อนหน้า",
"sNext": "ถัดไป",
"sLast": "สุดท้าย"
}
},
"pageLength": 25,
"columnDefs": [
{ "orderable": false, "targets": 5 }
],
"order": [[ 0, "asc" ]]
});
// Checkbox toggle prefix
$('#togglePrefix').on('change', function() {
if ($(this).is(':checked')) {
$('.name-prefix').removeClass('hidden');
} else {
$('.name-prefix').addClass('hidden');
}
});
});
</script>
<?php require_once APP_ROOT . '/app/Views/layouts/footer.php'; ?>
@@ -0,0 +1,408 @@
<?php require_once APP_ROOT . '/app/Views/layouts/header.php'; ?>
<?php
$thaiMonths = [
1 => 'มกราคม', 2 => 'กุมภาพันธ์', 3 => 'มีนาคม', 4 => 'เมษายน',
5 => 'พฤษภาคม', 6 => 'มิถุนายน', 7 => 'กรกฎาคม', 8 => 'สิงหาคม',
9 => 'กันยายน', 10 => 'ตุลาคม', 11 => 'พฤศจิกายน', 12 => 'ธันวาคม'
];
$yearBe = $period['year_no'] > 2500 ? $period['year_no'] : $period['year_no'] + 543;
$periodName = $thaiMonths[$period['month_no']] . ' ' . $yearBe;
// Prepare options for javascript
$incomeOptions = '<option value="">-- เลือกรายการรับ --</option>';
foreach ($masterIncomes as $mi) {
$incomeOptions .= '<option value="' . $mi['id'] . '">' . htmlspecialchars($mi['code'] . ' - ' . $mi['name']) . '</option>';
}
$deductionOptions = '<option value="">-- เลือกรายการจ่าย --</option>';
foreach ($masterDeductions as $md) {
$deductionOptions .= '<option value="' . $md['id'] . '">' . htmlspecialchars($md['code'] . ' - ' . $md['name']) . '</option>';
}
?>
<div class="mb-6">
<div class="flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400 mb-2">
<a href="<?= BASE_URL ?>/payroll" class="hover:text-sky-500 transition-colors">จัดการงวดเงินเดือน</a>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-3 h-3"><path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" /></svg>
<a href="<?= BASE_URL ?>/payroll/details/<?= $period['id'] ?>" class="hover:text-sky-500 transition-colors">รายละเอียดงวด</a>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-3 h-3"><path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" /></svg>
<span class="text-slate-800 dark:text-slate-200 font-medium">แก้ไขข้อมูลพนักงาน</span>
</div>
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h2 class="text-2xl font-bold text-slate-800 dark:text-white">แก้ไขข้อมูลรายบุคคล</h2>
<p class="text-slate-500 dark:text-slate-400 text-sm mt-1">ประจำเดือน <?= $periodName ?></p>
</div>
<a href="<?= BASE_URL ?>/payroll/details/<?= $period['id'] ?>" class="inline-flex items-center px-4 py-2 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl text-sm font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors shadow-sm">
ย้อนกลับ
</a>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6 relative">
<!-- Profile Card (Left) -->
<div class="lg:col-span-1 space-y-6">
<div class="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-sm border border-slate-100 dark:border-slate-700">
<div class="flex flex-col items-center text-center">
<div class="w-24 h-24 mb-4 relative">
<img class="w-24 h-24 rounded-full object-cover bg-sky-50 border-4 border-white dark:border-slate-800 shadow-sm" src="<?= BASE_URL ?>/employee/image/<?= htmlspecialchars($employee['national_id']) ?>" alt="<?= htmlspecialchars($employee['first_name']) ?>" onerror="this.src='data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 24 24\' fill=\'%2394a3b8\'><path fill-rule=\'evenodd\' d=\'M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z\' clip-rule=\'evenodd\'/></svg>'">
<div class="absolute inset-0 rounded-full shadow-inner pointer-events-none"></div>
</div>
<h3 class="text-lg font-bold text-slate-800 dark:text-white"><?= htmlspecialchars($employee['first_name'] . ' ' . $employee['last_name']) ?></h3>
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1"><?= htmlspecialchars($employee['position'] ?: 'ไม่ระบุตำแหน่ง') ?></p>
<div class="mt-4 w-full bg-slate-50 dark:bg-slate-700/50 rounded-xl p-3 text-left">
<div class="text-xs text-slate-500 dark:text-slate-400 mb-1">เลขบัตรประชาชน</div>
<div class="text-sm font-medium text-slate-800 dark:text-slate-200 font-mono"><?= htmlspecialchars($employee['national_id']) ?></div>
<div class="text-xs text-slate-500 dark:text-slate-400 mt-3 mb-1">แผนก</div>
<div class="text-sm font-medium text-slate-800 dark:text-slate-200"><?= htmlspecialchars($employee['department'] ?: '-') ?></div>
<div class="text-xs text-slate-500 dark:text-slate-400 mt-3 mb-1">ธนาคาร</div>
<input type="text" name="bank_name" form="editEmployeeForm" value="<?= htmlspecialchars($employee['bank_name'] ?? 'ธนาคารกรุงไทย จำกัด(มหาชน)') ?>" placeholder="ชื่อธนาคาร" class="w-full text-sm rounded-md border-0 py-1.5 px-2 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-inset focus:ring-sky-500 bg-white dark:bg-slate-800 mb-2">
<div class="text-xs text-slate-500 dark:text-slate-400 mb-1">เลขที่บัญชี</div>
<input type="text" name="bank_account" form="editEmployeeForm" value="<?= htmlspecialchars($employee['bank_account'] ?? '') ?>" placeholder="เลขที่บัญชี" class="w-full text-sm rounded-md border-0 py-1.5 px-2 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-inset focus:ring-sky-500 bg-white dark:bg-slate-800 font-mono">
</div>
</div>
</div>
<!-- Summary Card -->
<div class="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-sm border border-slate-100 dark:border-slate-700 sticky top-4">
<h4 class="text-sm font-semibold text-slate-800 dark:text-white uppercase tracking-wider mb-4">สรุปยอดเงิน</h4>
<div class="space-y-3 text-sm">
<div class="flex justify-between items-center text-emerald-600 dark:text-emerald-400">
<span>รายรับรวม</span>
<span class="font-medium" id="sumTotalIncome">+ 0.00</span>
</div>
<div class="flex justify-between items-center text-red-500 dark:text-red-400">
<span>รายจ่ายรวม</span>
<span class="font-medium" id="sumTotalDeduction">- 0.00</span>
</div>
<div class="pt-3 mt-3 border-t border-slate-100 dark:border-slate-700 flex justify-between items-center">
<span class="font-bold text-slate-800 dark:text-white">ยอดรับสุทธิ</span>
<span class="text-lg font-bold text-sky-600 dark:text-sky-400" id="sumNetSalary">0.00</span>
</div>
</div>
</div>
</div>
<!-- Form Data (Right) -->
<div class="lg:col-span-3">
<form action="<?= BASE_URL ?>/payroll/employee/update" method="POST" id="editEmployeeForm" class="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-100 dark:border-slate-700">
<input type="hidden" name="employee_salary_id" value="<?= $employee['id'] ?>">
<input type="hidden" name="salary_month_id" value="<?= $employee['salary_month_id'] ?>">
<div class="p-6 border-b border-slate-100 dark:border-slate-700 bg-slate-50/50 dark:bg-slate-900/20">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-emerald-600 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /></svg>
รายการรับ (Incomes)
</h3>
<button type="button" onclick="addIncomeRow()" class="text-sm text-emerald-600 hover:text-emerald-700 font-medium px-3 py-1.5 rounded-lg hover:bg-emerald-50 dark:hover:bg-emerald-900/30 transition-colors">+ เพิ่มรายการรับ</button>
</div>
<div class="space-y-3" id="incomeContainer">
<?php if (empty($incomes)): ?>
<div class="text-sm text-slate-400 italic text-center py-4 empty-msg">ไม่มีรายการรับ</div>
<?php else: ?>
<?php foreach ($incomes as $inc): ?>
<div class="flex items-center gap-3 bg-white dark:bg-slate-800 p-3 rounded-xl border border-slate-200 dark:border-slate-700 income-row">
<div class="flex-1">
<select name="income_id[]" class="searchable-select w-full" required>
<option value="">-- เลือกรายการรับ --</option>
<?php foreach ($masterIncomes as $mi): ?>
<option value="<?= $mi['id'] ?>" <?= $mi['id'] == $inc['income_master_id'] ? 'selected' : '' ?>><?= htmlspecialchars($mi['code'] . ' - ' . $mi['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="w-40">
<input type="text" name="income_amount[]" value="<?= number_format($inc['amount'], 2) ?>" oninput="calculateTotal()" class="w-full rounded-lg border-0 py-2 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-inset focus:ring-sky-500 sm:text-sm bg-slate-50 dark:bg-slate-900 font-mono text-right number-input" required>
</div>
<button type="button" onclick="this.closest('.income-row').remove(); calculateTotal(); checkEmpty('income');" class="p-2 text-slate-400 hover:text-red-500 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" /></svg>
</button>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
<div class="p-6 border-b border-slate-100 dark:border-slate-700 bg-slate-50/50 dark:bg-slate-900/20">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-red-500 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14" /></svg>
รายการจ่าย (Deductions)
</h3>
<button type="button" onclick="addDeductionRow()" class="text-sm text-red-500 hover:text-red-600 font-medium px-3 py-1.5 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/30 transition-colors">+ เพิ่มรายการจ่าย</button>
</div>
<div class="space-y-3" id="deductionContainer">
<?php if (empty($deductions)): ?>
<div class="text-sm text-slate-400 italic text-center py-4 empty-msg">ไม่มีรายการจ่าย</div>
<?php else: ?>
<?php foreach ($deductions as $ded): ?>
<div class="flex items-center gap-3 bg-white dark:bg-slate-800 p-3 rounded-xl border border-slate-200 dark:border-slate-700 deduction-row">
<div class="flex-1">
<select name="deduction_id[]" class="searchable-select w-full" required>
<option value="">-- เลือกรายการจ่าย --</option>
<?php foreach ($masterDeductions as $md): ?>
<option value="<?= $md['id'] ?>" <?= $md['id'] == $ded['deduction_master_id'] ? 'selected' : '' ?>><?= htmlspecialchars($md['code'] . ' - ' . $md['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="w-40">
<input type="text" name="deduction_amount[]" value="<?= number_format($ded['amount'], 2) ?>" oninput="calculateTotal()" class="w-full rounded-lg border-0 py-2 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-inset focus:ring-sky-500 sm:text-sm bg-slate-50 dark:bg-slate-900 font-mono text-right number-input" required>
</div>
<button type="button" onclick="this.closest('.deduction-row').remove(); calculateTotal(); checkEmpty('deduction');" class="p-2 text-slate-400 hover:text-red-500 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" /></svg>
</button>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
<div class="p-6 flex items-center justify-end gap-3 bg-slate-50 dark:bg-slate-800 rounded-b-2xl">
<a href="<?= BASE_URL ?>/payroll/details/<?= $period['id'] ?>" class="px-5 py-2.5 bg-white dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-xl text-sm font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-600 transition-colors shadow-sm">
ยกเลิก
</a>
<button type="submit" class="px-5 py-2.5 bg-sky-500 hover:bg-sky-600 text-white rounded-xl text-sm font-medium transition-colors shadow-sm flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" /></svg>
บันทึกข้อมูล
</button>
</div>
</form>
</div>
</div>
<script>
// Master data options for JS
const incomeOptions = `<?= $incomeOptions ?>`;
const deductionOptions = `<?= $deductionOptions ?>`;
function addIncomeRow() {
const container = document.getElementById('incomeContainer');
const emptyMsg = container.querySelector('.empty-msg');
if (emptyMsg) emptyMsg.remove();
const row = document.createElement('div');
row.className = 'flex items-center gap-3 bg-white dark:bg-slate-800 p-3 rounded-xl border border-slate-200 dark:border-slate-700 income-row';
row.innerHTML = `
<div class="flex-1">
<select name="income_id[]" class="searchable-select w-full" required>
${incomeOptions}
</select>
</div>
<div class="w-40">
<input type="text" name="income_amount[]" value="0.00" oninput="calculateTotal()" class="w-full rounded-lg border-0 py-2 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-inset focus:ring-sky-500 sm:text-sm bg-slate-50 dark:bg-slate-900 font-mono text-right number-input" required>
</div>
<button type="button" onclick="this.closest('.income-row').remove(); calculateTotal(); checkEmpty('income');" class="p-2 text-slate-400 hover:text-red-500 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" /></svg>
</button>
`;
container.appendChild(row);
initNumberFormat(row.querySelector('.number-input'));
// Initialize Select2 on the new row
$(row).find('.searchable-select').select2({
width: '100%',
matcher: function(params, data) {
if ($.trim(params.term) === '') return data;
if (typeof data.text === 'undefined') return null;
var q = params.term.toLowerCase();
if (data.text.toLowerCase().indexOf(q) > -1 || (data.id && data.id.toLowerCase().indexOf(q) > -1)) {
return $.extend({}, data, true);
}
return null;
}
});
}
function addDeductionRow() {
const container = document.getElementById('deductionContainer');
const emptyMsg = container.querySelector('.empty-msg');
if (emptyMsg) emptyMsg.remove();
const row = document.createElement('div');
row.className = 'flex items-center gap-3 bg-white dark:bg-slate-800 p-3 rounded-xl border border-slate-200 dark:border-slate-700 deduction-row';
row.innerHTML = `
<div class="flex-1">
<select name="deduction_id[]" class="searchable-select w-full" required>
${deductionOptions}
</select>
</div>
<div class="w-40">
<input type="text" name="deduction_amount[]" value="0.00" oninput="calculateTotal()" class="w-full rounded-lg border-0 py-2 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-inset focus:ring-sky-500 sm:text-sm bg-slate-50 dark:bg-slate-900 font-mono text-right number-input" required>
</div>
<button type="button" onclick="this.closest('.deduction-row').remove(); calculateTotal(); checkEmpty('deduction');" class="p-2 text-slate-400 hover:text-red-500 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" /></svg>
</button>
`;
container.appendChild(row);
initNumberFormat(row.querySelector('.number-input'));
// Initialize Select2 on the new row
$(row).find('.searchable-select').select2({
width: '100%',
matcher: function(params, data) {
if ($.trim(params.term) === '') return data;
if (typeof data.text === 'undefined') return null;
var q = params.term.toLowerCase();
if (data.text.toLowerCase().indexOf(q) > -1 || (data.id && data.id.toLowerCase().indexOf(q) > -1)) {
return $.extend({}, data, true);
}
return null;
}
});
}
function checkEmpty(type) {
const container = document.getElementById(type + 'Container');
const rows = container.querySelectorAll('.' + type + '-row');
if (rows.length === 0) {
container.innerHTML = `<div class="text-sm text-slate-400 italic text-center py-4 empty-msg">ไม่มีรายการ${type === 'income' ? 'รับ' : 'จ่าย'}</div>`;
}
}
function parseAmt(val) {
if (!val) return 0;
return parseFloat(val.toString().replace(/,/g, '')) || 0;
}
function formatNumber(num) {
return num.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2});
}
function calculateTotal() {
let totalIncome = 0;
document.querySelectorAll('input[name="income_amount[]"]').forEach(input => {
totalIncome += parseAmt(input.value);
});
let totalDeduction = 0;
document.querySelectorAll('input[name="deduction_amount[]"]').forEach(input => {
totalDeduction += parseAmt(input.value);
});
const netSalary = totalIncome - totalDeduction;
document.getElementById('sumTotalIncome').innerText = '+ ' + formatNumber(totalIncome);
document.getElementById('sumTotalDeduction').innerText = '- ' + formatNumber(totalDeduction);
document.getElementById('sumNetSalary').innerText = formatNumber(netSalary);
}
// Auto-format numbers on blur
function initNumberFormat(el) {
el.addEventListener('blur', function() {
const val = parseAmt(this.value);
this.value = formatNumber(val);
calculateTotal();
});
// Allow only numbers and dots
el.addEventListener('input', function() {
let val = this.value;
val = val.replace(/[^0-9.,]/g, '');
this.value = val;
});
}
document.querySelectorAll('.number-input').forEach(initNumberFormat);
// Initial calculation
calculateTotal();
</script>
<!-- Select2 for Searchable Dropdowns -->
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<style type="text/tailwindcss">
/* Select2 Tailwind Override */
.select2-container--default .select2-selection--single {
@apply w-full rounded-lg py-2 px-3 text-slate-900 shadow-sm ring-1 ring-inset ring-slate-300 focus:ring-2 focus:ring-inset focus:ring-sky-500 sm:text-sm bg-slate-50 outline-none transition-all;
border: none !important;
border-radius: 0.5rem !important;
height: 36px !important;
}
.select2-container--default.select2-container--open .select2-selection--single {
@apply ring-2 ring-inset ring-sky-500;
}
.select2-container--default .select2-selection--single .select2-selection__rendered {
line-height: 20px;
padding-left: 0;
@apply text-slate-900;
}
.select2-container--default .select2-selection--single .select2-selection__arrow {
height: 34px;
right: 8px;
}
.select2-dropdown {
@apply border-0 shadow-lg rounded-lg overflow-hidden ring-1 ring-slate-200 mt-1;
}
.select2-search--dropdown {
@apply p-2 bg-slate-50 border-b border-slate-100;
}
.select2-search--dropdown .select2-search__field {
@apply w-full rounded-md border-0 py-1.5 px-3 text-slate-900 shadow-sm ring-1 ring-inset ring-slate-300 focus:ring-2 focus:ring-inset focus:ring-sky-500 sm:text-sm;
}
.select2-results__option {
@apply px-3 py-2 text-sm text-slate-700 cursor-pointer transition-colors;
}
.select2-container--default .select2-results__option--highlighted[aria-selected] {
@apply bg-sky-50 text-sky-700;
}
.select2-container--default .select2-results__option[aria-selected=true] {
@apply bg-slate-100 text-slate-900 font-medium;
}
/* Dark Mode Support */
.dark .select2-container--default .select2-selection--single {
@apply bg-slate-900 ring-slate-600 text-white;
}
.dark .select2-container--default .select2-selection--single .select2-selection__rendered {
@apply text-white;
}
.dark .select2-dropdown {
@apply bg-slate-800 ring-slate-700;
}
.dark .select2-search--dropdown {
@apply bg-slate-800 border-slate-700;
}
.dark .select2-search--dropdown .select2-search__field {
@apply bg-slate-900 ring-slate-700 text-white focus:ring-sky-500;
}
.dark .select2-results__option {
@apply text-slate-300;
}
.dark .select2-container--default .select2-results__option--highlighted[aria-selected] {
@apply bg-slate-700 text-white;
}
.dark .select2-container--default .select2-results__option[aria-selected=true] {
@apply bg-slate-900 text-white;
}
</style>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script>
$(document).ready(function() {
// Initialize existing selects
$('.searchable-select').select2({
width: '100%',
matcher: function(params, data) {
// Custom matcher for better search (ignores spaces etc if needed)
if ($.trim(params.term) === '') {
return data;
}
if (typeof data.text === 'undefined') {
return null;
}
var q = params.term.toLowerCase();
if (data.text.toLowerCase().indexOf(q) > -1 || (data.id && data.id.toLowerCase().indexOf(q) > -1)) {
return $.extend({}, data, true);
}
return null;
}
});
});
</script>
<?php require_once APP_ROOT . '/app/Views/layouts/footer.php'; ?>
@@ -0,0 +1,169 @@
<?php require_once APP_ROOT . '/app/Views/layouts/header.php'; ?>
<?php
$thaiMonths = [
1 => 'มกราคม', 2 => 'กุมภาพันธ์', 3 => 'มีนาคม', 4 => 'เมษายน',
5 => 'พฤษภาคม', 6 => 'มิถุนายน', 7 => 'กรกฎาคม', 8 => 'สิงหาคม',
9 => 'กันยายน', 10 => 'ตุลาคม', 11 => 'พฤศจิกายน', 12 => 'ธันวาคม'
];
function getStatusBadge($status) {
switch ($status) {
case 'Draft': return '<span class="px-2.5 py-1 text-xs font-semibold rounded-full bg-slate-100 text-slate-700 dark:bg-slate-700 dark:text-slate-300">ฉบับร่าง</span>';
case 'Imported': return '<span class="px-2.5 py-1 text-xs font-semibold rounded-full bg-sky-100 text-sky-700 dark:bg-sky-900/50 dark:text-sky-400">นำเข้าข้อมูลแล้ว</span>';
case 'Pending Review': return '<span class="px-2.5 py-1 text-xs font-semibold rounded-full bg-amber-100 text-amber-700 dark:bg-amber-900/50 dark:text-amber-400">รอตรวจสอบ</span>';
case 'HR Approved': return '<span class="px-2.5 py-1 text-xs font-semibold rounded-full bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-400">HR อนุมัติ</span>';
case 'Finance Approved': return '<span class="px-2.5 py-1 text-xs font-semibold rounded-full bg-indigo-100 text-indigo-700 dark:bg-indigo-900/50 dark:text-indigo-400">การเงินอนุมัติ</span>';
case 'Director Approved': return '<span class="px-2.5 py-1 text-xs font-semibold rounded-full bg-emerald-100 text-emerald-700 dark:bg-emerald-900/50 dark:text-emerald-400">ผอ.อนุมัติ</span>';
case 'Locked': return '<span class="px-2.5 py-1 text-xs font-semibold rounded-full bg-zinc-800 text-zinc-100 dark:bg-zinc-900 dark:text-zinc-300">ล็อก</span>';
case 'Published': return '<span class="px-2.5 py-1 text-xs font-semibold rounded-full bg-teal-100 text-teal-700 dark:bg-teal-900/50 dark:text-teal-400">ประกาศใช้</span>';
case 'Cancelled': return '<span class="px-2.5 py-1 text-xs font-semibold rounded-full bg-red-100 text-red-700 dark:bg-red-900/50 dark:text-red-400">ยกเลิก</span>';
default: return '<span class="px-2.5 py-1 text-xs font-semibold rounded-full bg-slate-100 text-slate-700 dark:bg-slate-700 dark:text-slate-300">' . htmlspecialchars($status) . '</span>';
}
}
?>
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-6 gap-4">
<div>
<h2 class="text-2xl font-bold text-slate-800 dark:text-white">จัดการงวดเงินเดือน</h2>
<p class="text-slate-500 dark:text-slate-400 text-sm mt-1">สร้างและจัดการรอบบิลเงินเดือนของแต่ละเดือน</p>
</div>
<button onclick="document.getElementById('createPeriodModal').classList.remove('hidden')" class="inline-flex items-center justify-center px-4 py-2.5 bg-sky-500 hover:bg-sky-600 text-white text-sm font-medium rounded-xl transition-colors shadow-sm w-full md:w-auto">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /></svg>
สร้างงวดใหม่
</button>
</div>
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-100 dark:border-slate-700 overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full text-left text-sm text-slate-600 dark:text-slate-300">
<thead class="bg-slate-50 dark:bg-slate-900/50 text-slate-500 dark:text-slate-400 text-xs uppercase font-semibold">
<tr>
<th class="px-6 py-4">ปี / เดือน</th>
<th class="px-6 py-4">อัปเดตล่าสุด</th>
<th class="px-6 py-4">สถานะ</th>
<th class="px-6 py-4 text-right">จัดการ</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 dark:divide-slate-700">
<?php if (empty($periods)): ?>
<tr>
<td colspan="4" class="px-6 py-8 text-center text-slate-500 dark:text-slate-400">
ยังไม่มีข้อมูลงวดเงินเดือน <button onclick="document.getElementById('createPeriodModal').classList.remove('hidden')" class="text-sky-500 hover:underline">สร้างงวดแรกที่นี่</button>
</td>
</tr>
<?php else: ?>
<?php foreach ($periods as $period): ?>
<tr class="hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors">
<td class="px-6 py-4 font-bold text-sky-600 dark:text-sky-400">
<?= $thaiMonths[$period['month_no']] ?> <?= $period['year_no'] > 2500 ? $period['year_no'] : $period['year_no'] + 543 ?>
</td>
<td class="px-6 py-4 text-xs">
<?= date('d/m/Y H:i', strtotime($period['updated_at'])) ?>
</td>
<td class="px-6 py-4">
<?= getStatusBadge($period['status']) ?>
</td>
<td class="px-6 py-4 text-right">
<?php if ($period['status'] !== 'Locked' && $period['status'] !== 'Published'): ?>
<button type="button" onclick="confirmClearData(<?= $period['id'] ?>, '<?= $thaiMonths[$period['month_no']] ?> <?= $period['year_no'] > 2500 ? $period['year_no'] : $period['year_no'] + 543 ?>')" class="inline-flex items-center px-3 py-1.5 border border-red-200 dark:border-red-900/50 bg-red-50 dark:bg-red-900/20 rounded-lg text-xs font-medium text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900/40 transition-colors mr-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-1.5"><path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" /></svg>
ล้างข้อมูล
</button>
<?php endif; ?>
<a href="<?= BASE_URL ?>/payroll/details/<?= $period['id'] ?>" class="inline-flex items-center px-3 py-1.5 border border-slate-300 dark:border-slate-600 rounded-lg text-xs font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 0 1 1.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 0 1-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 0 1-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 0 1 .12-1.45l.773-.773a1.125 1.125 0 0 1 1.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /></svg>
จัดการ
</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<!-- Tailwind Modal -->
<div id="createPeriodModal" class="hidden fixed inset-0 z-50 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<!-- Backdrop -->
<div class="fixed inset-0 bg-slate-900/50 backdrop-blur-sm transition-opacity" aria-hidden="true" onclick="document.getElementById('createPeriodModal').classList.add('hidden')"></div>
<div class="flex min-h-screen items-center justify-center p-4 text-center sm:p-0">
<div class="relative transform overflow-hidden rounded-2xl bg-white dark:bg-slate-800 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg border border-slate-100 dark:border-slate-700">
<form action="<?= BASE_URL ?>/payroll/store" method="POST">
<div class="bg-white dark:bg-slate-800 px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div class="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-sky-100 dark:bg-sky-900/50 sm:mx-0 sm:h-10 sm:w-10">
<svg class="h-6 w-6 text-sky-600 dark:text-sky-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
</div>
<div class="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left w-full">
<h3 class="text-lg font-semibold leading-6 text-slate-900 dark:text-white" id="modal-title">สร้างงวดเงินเดือนใหม่</h3>
<div class="mt-6 space-y-4">
<div>
<label for="year_no" class="block text-sm font-medium leading-6 text-slate-700 dark:text-slate-300 mb-2">ปี พ.ศ. (Year)</label>
<input type="number" id="year_no" name="year_no" value="<?= date('Y') + 543 ?>" required min="2500" max="2600" class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 placeholder:text-slate-400 focus:ring-2 focus:ring-inset focus:ring-sky-500 sm:text-sm sm:leading-6 bg-slate-50 dark:bg-slate-900">
</div>
<div>
<label for="month_no" class="block text-sm font-medium leading-6 text-slate-700 dark:text-slate-300 mb-2">เดือน (Month)</label>
<select id="month_no" name="month_no" required class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-inset focus:ring-sky-500 sm:text-sm sm:leading-6 bg-slate-50 dark:bg-slate-900">
<option value="">-- เลือกเดือน --</option>
<?php foreach ($thaiMonths as $num => $name): ?>
<option value="<?= $num ?>" <?= (date('n') == $num) ? 'selected' : '' ?>><?= $name ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="rounded-xl bg-sky-50 dark:bg-sky-900/30 p-4 mt-6">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-sky-400" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M19 10.5a8.5 8.5 0 1 1-17 0 8.5 8.5 0 0 1 17 0ZM8.25 9.75A.75.75 0 0 1 9 9h.253a1.75 1.75 0 0 1 1.709 2.13l-.46 2.066a.25.25 0 0 0 .245.304H11a.75.75 0 0 1 0 1.5h-.253a1.75 1.75 0 0 1-1.709-2.13l.46-2.066a.25.25 0 0 0-.245-.304H9a.75.75 0 0 1-.75-.75ZM10 7a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z" clip-rule="evenodd" /></svg>
</div>
<div class="ml-3">
<p class="text-sm text-sky-700 dark:text-sky-300 m-0">งวดใหม่จะถูกสร้างในสถานะ <strong>ฉบับร่าง (Draft)</strong></p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="bg-slate-50 dark:bg-slate-700/50 px-4 py-4 sm:flex sm:flex-row-reverse sm:px-6">
<button type="submit" class="inline-flex w-full justify-center rounded-xl bg-sky-500 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-sky-600 sm:ml-3 sm:w-auto transition-colors">ยืนยันสร้างงวด</button>
<button type="button" onclick="document.getElementById('createPeriodModal').classList.add('hidden')" class="mt-3 inline-flex w-full justify-center rounded-xl bg-white dark:bg-slate-800 px-4 py-2.5 text-sm font-semibold text-slate-900 dark:text-slate-300 shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 hover:bg-slate-50 dark:hover:bg-slate-700 sm:mt-0 sm:w-auto transition-colors">ยกเลิก</button>
</div>
</form>
</div>
</div>
</div>
<!-- Form for Clearing Data -->
<form id="clearDataForm" action="<?= BASE_URL ?>/payroll/clear" method="POST" class="hidden">
<input type="hidden" name="salary_month_id" id="clearSalaryMonthId" value="">
</form>
<script>
function confirmClearData(id, periodName) {
Swal.fire({
title: 'ยืนยันการล้างข้อมูล?',
html: `คุณต้องการล้างข้อมูลที่นำเข้าทั้งหมดของงวด <b>${periodName}</b> ใช่หรือไม่?<br><span class="text-red-500 text-sm">คำเตือน: ข้อมูลเงินเดือนของพนักงานทุกคนในงวดนี้จะถูกลบทั้งหมด</span>`,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#ef4444',
cancelButtonColor: '#64748b',
confirmButtonText: 'ใช่, ล้างข้อมูล',
cancelButtonText: 'ยกเลิก'
}).then((result) => {
if (result.isConfirmed) {
document.getElementById('clearSalaryMonthId').value = id;
document.getElementById('clearDataForm').submit();
}
});
}
</script>
<?php require_once APP_ROOT . '/app/Views/layouts/footer.php'; ?>
@@ -0,0 +1,101 @@
<?php require_once APP_ROOT . '/app/Views/layouts/header.php'; ?>
<div class="max-w-4xl mx-auto">
<div class="mb-6">
<h2 class="text-2xl font-bold text-slate-800 dark:text-white">ข้อมูลส่วนตัว (Profile)</h2>
<p class="text-slate-500 dark:text-slate-400 text-sm mt-1">จัดการข้อมูลส่วนตัวและเปลี่ยนรหัสผ่านของคุณ</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- Left Column: Personal Info -->
<div class="md:col-span-1 space-y-6">
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-100 dark:border-slate-700 p-6">
<div class="flex flex-col items-center">
<div class="w-24 h-24 rounded-full bg-gradient-to-tr from-sky-400 to-indigo-500 flex items-center justify-center text-white text-3xl font-bold shadow-lg shadow-sky-500/30 mb-4">
<?= mb_substr(htmlspecialchars($user['first_name'] ?? 'U'), 0, 1) ?>
</div>
<h3 class="text-lg font-bold text-slate-800 dark:text-white">
<?= htmlspecialchars($user['first_name'] . ' ' . $user['last_name']) ?>
</h3>
<p class="text-sm font-medium text-sky-600 dark:text-sky-400 mt-1"><?= htmlspecialchars($roleName) ?></p>
</div>
<div class="mt-8 space-y-4">
<div>
<label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1">Username</label>
<div class="text-sm font-medium text-slate-700 dark:text-slate-300 bg-slate-50 dark:bg-slate-900/50 px-3 py-2 rounded-lg border border-slate-100 dark:border-slate-700">
<?= htmlspecialchars($user['username']) ?>
</div>
</div>
<div>
<label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1">ชื่อ</label>
<div class="text-sm font-medium text-slate-700 dark:text-slate-300 bg-slate-50 dark:bg-slate-900/50 px-3 py-2 rounded-lg border border-slate-100 dark:border-slate-700">
<?= htmlspecialchars($user['first_name']) ?>
</div>
</div>
<div>
<label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1">นามสกุล</label>
<div class="text-sm font-medium text-slate-700 dark:text-slate-300 bg-slate-50 dark:bg-slate-900/50 px-3 py-2 rounded-lg border border-slate-100 dark:border-slate-700">
<?= htmlspecialchars($user['last_name']) ?>
</div>
</div>
</div>
</div>
</div>
<!-- Right Column: Password Change -->
<div class="md:col-span-2">
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-100 dark:border-slate-700 p-6">
<h3 class="text-lg font-bold text-slate-800 dark:text-white mb-6 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 mr-2 text-sky-500"><path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z" /></svg>
เปลี่ยนรหัสผ่าน
</h3>
<form action="<?= BASE_URL ?>/profile/update-password" method="POST" class="space-y-5">
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">รหัสผ่านปัจจุบัน <span class="text-red-500">*</span></label>
<input type="password" name="current_password" required
class="w-full px-4 py-2.5 rounded-xl border border-slate-200 dark:border-slate-600 bg-slate-50 focus:bg-white dark:bg-slate-900/50 dark:focus:bg-slate-800 text-slate-800 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-sky-500/50 focus:border-sky-500 transition-all">
</div>
<hr class="border-slate-100 dark:border-slate-700">
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">รหัสผ่านใหม่ <span class="text-red-500">*</span></label>
<input type="password" name="new_password" required minlength="6"
class="w-full px-4 py-2.5 rounded-xl border border-slate-200 dark:border-slate-600 bg-slate-50 focus:bg-white dark:bg-slate-900/50 dark:focus:bg-slate-800 text-slate-800 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-sky-500/50 focus:border-sky-500 transition-all">
<p class="text-xs text-slate-500 mt-1">ต้องมีความยาวอย่างน้อย 6 ตัวอักษร</p>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">ยืนยันรหัสผ่านใหม่ <span class="text-red-500">*</span></label>
<input type="password" name="confirm_password" required minlength="6"
class="w-full px-4 py-2.5 rounded-xl border border-slate-200 dark:border-slate-600 bg-slate-50 focus:bg-white dark:bg-slate-900/50 dark:focus:bg-slate-800 text-slate-800 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-sky-500/50 focus:border-sky-500 transition-all">
</div>
<div class="pt-4">
<button type="submit" class="w-full md:w-auto px-6 py-2.5 bg-sky-500 hover:bg-sky-600 text-white font-medium rounded-xl transition-colors shadow-sm shadow-sky-500/20">
บันทึกการเปลี่ยนรหัสผ่าน
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<script>
document.querySelector('form').addEventListener('submit', function(e) {
const newPass = document.querySelector('input[name="new_password"]').value;
const confPass = document.querySelector('input[name="confirm_password"]').value;
if (newPass !== confPass) {
e.preventDefault();
alert('รหัสผ่านใหม่และการยืนยันรหัสผ่านไม่ตรงกัน กรุณาตรวจสอบอีกครั้ง');
}
});
</script>
<?php require_once APP_ROOT . '/app/Views/layouts/footer.php'; ?>
@@ -0,0 +1,221 @@
<?php require_once APP_ROOT . '/app/Views/layouts/header.php'; ?>
<div class="mb-6 flex justify-between items-start md:items-center flex-col md:flex-row gap-4">
<div>
<h2 class="text-2xl font-bold text-slate-800 dark:text-white">ระบบสืบค้นและพิมพ์สลิปเงินเดือน / 50 ทวิ</h2>
<p class="text-slate-500 dark:text-slate-400 text-sm mt-1">ค้นหารายการจ่ายเงินเดือนเพื่อสั่งพิมพ์สลิปเงินเดือน (รายเดือน) หรือใบเสียภาษี (50 ทวิ - รายปี)</p>
</div>
<div class="flex flex-col items-end gap-2">
<label class="inline-flex items-center cursor-pointer mb-2">
<input type="checkbox" id="togglePrefix" class="w-4 h-4 text-indigo-600 bg-slate-100 border-slate-300 rounded focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:ring-offset-slate-800 focus:ring-2 dark:bg-slate-700 dark:border-slate-600">
<span class="ml-2 text-sm font-medium text-slate-700 dark:text-slate-300">แสดงคำนำหน้าชื่อ</span>
</label>
<label class="inline-flex items-center cursor-pointer">
<input type="checkbox" id="groupByLetter" class="w-4 h-4 text-indigo-600 bg-slate-100 border-slate-300 rounded focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:ring-offset-slate-800 focus:ring-2 dark:bg-slate-700 dark:border-slate-600">
<span class="ml-2 text-sm font-medium text-slate-700 dark:text-slate-300">จัดกลุ่มตามตัวอักษร (มีใบปะหน้า)</span>
</label>
<div>
<form id="bulkTaxForm" action="<?= BASE_URL ?>/reports/taxBulk" method="POST" target="_blank" class="hidden">
<input type="hidden" name="year" value="<?= htmlspecialchars($year ?? date('Y')) ?>">
<input type="hidden" name="group_by_letter" id="formGroupByLetter" value="0">
<div id="bulkTaxFormContainer"></div>
</form>
<button type="button" id="bulkPrintBtn" class="inline-flex items-center justify-center px-4 py-2.5 bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-medium rounded-xl transition-colors shadow-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M6.72 13.829c-.24.03-.48.062-.72.096m.72-.096a42.415 42.415 0 0 1 10.56 0m-10.56 0L6.34 18m10.94-4.171c.24.03.48.062.72.096m-.72-.096L17.66 18m0 0 .229 2.523a1.125 1.125 0 0 1-1.12 1.227H7.231c-.662 0-1.18-.568-1.12-1.227L6.34 18m11.318 0h1.091A2.25 2.25 0 0 0 21 15.75V9.456c0-1.081-.768-2.015-1.837-2.175a48.055 48.055 0 0 0-1.913-.247M6.34 18H5.25A2.25 2.25 0 0 1 3 15.75V9.456c0-1.081.768-2.015 1.837-2.175a48.041 48.041 0 0 1 1.913-.247m10.5 0a48.536 48.536 0 0 0-10.5 0m10.5 0V3.375c0-.621-.504-1.125-1.125-1.125h-8.25c-.621 0-1.125.504-1.125 1.125v3.659M18 10.5h.008v.008H18V10.5Zm-3 0h.008v.008H15V10.5Z" /></svg>
พิมพ์ 50 ทวิ ที่เลือก (Bulk Print)
</button>
</div>
</div>
</div>
<!-- Filter Section -->
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-100 dark:border-slate-700 p-6 mb-6">
<form action="<?= BASE_URL ?>/reports" method="GET" class="flex flex-col md:flex-row gap-4 items-end">
<div class="w-full md:w-1/3">
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">เลือกปี (พ.ศ.)</label>
<select name="year" class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-sky-500 bg-slate-50 dark:bg-slate-900 sm:text-sm">
<?php
$currentYear = date('Y');
for($y = $currentYear + 1; $y >= $currentYear - 5; $y--):
$thaiYear = $y + 543;
?>
<option value="<?= $y ?>" <?= (isset($year) && $year == $y) ? 'selected' : '' ?>><?= $thaiYear ?></option>
<?php endfor; ?>
</select>
</div>
<div class="w-full md:w-1/3">
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">เลือกเดือน</label>
<select name="month" class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-sky-500 bg-slate-50 dark:bg-slate-900 sm:text-sm">
<?php
$thaiMonths = [
'01' => 'มกราคม', '02' => 'กุมภาพันธ์', '03' => 'มีนาคม', '04' => 'เมษายน',
'05' => 'พฤษภาคม', '06' => 'มิถุนายน', '07' => 'กรกฎาคม', '08' => 'สิงหาคม',
'09' => 'กันยายน', '10' => 'ตุลาคม', '11' => 'พฤศจิกายน', '12' => 'ธันวาคม'
];
foreach($thaiMonths as $mNum => $mName):
?>
<option value="<?= $mNum ?>" <?= (isset($month) && $month == $mNum) ? 'selected' : '' ?>><?= $mName ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="w-full md:w-1/3">
<button type="submit" class="w-full inline-flex items-center justify-center px-4 py-2.5 bg-sky-500 hover:bg-sky-600 text-white text-sm font-medium rounded-xl transition-colors shadow-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" /></svg>
ค้นหา
</button>
</div>
</form>
</div>
<!-- Results DataTables -->
<link rel="stylesheet" href="https://cdn.datatables.net/2.0.8/css/dataTables.tailwindcss.css">
<style>
/* DataTables Dark Mode Overrides */
.dark .dt-container { color: #cbd5e1; }
.dark .dt-length select, .dark .dt-search input { background-color: #1e293b; border-color: #334155; color: #f8fafc; border-radius: 0.5rem; padding: 0.25rem 0.5rem; }
.dark .dt-info, .dark .dt-paging-button { color: #94a3b8 !important; }
.dark .dt-paging-button.current { background: #38bdf8 !important; color: #0f172a !important; border-color: #38bdf8 !important; }
.dark .dt-paging-button:hover { background: #334155 !important; color: #f8fafc !important; }
table.dataTable.no-footer { border-bottom: 1px solid #334155 !important; }
</style>
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-100 dark:border-slate-700 overflow-hidden p-6">
<div class="overflow-x-hidden">
<table id="reportTable" class="w-full text-left text-sm text-slate-600 dark:text-slate-300 display">
<thead class="bg-slate-50 dark:bg-slate-900/50 text-slate-500 dark:text-slate-400 text-xs uppercase font-semibold">
<tr>
<th class="px-4 py-4 w-10 text-center">
<input type="checkbox" id="selectAllCheckbox" class="w-4 h-4 text-sky-600 bg-slate-100 border-slate-300 rounded focus:ring-sky-500 dark:focus:ring-sky-600 dark:ring-offset-slate-800 focus:ring-2 dark:bg-slate-700 dark:border-slate-600 cursor-pointer">
</th>
<th class="px-4 py-4">รูป</th>
<th class="px-6 py-4">ชื่อ-สกุล</th>
<th class="px-6 py-4">ตำแหน่ง/หน่วยงาน</th>
<th class="px-6 py-4 text-center">พิมพ์สลิป (รายเดือน)</th>
<th class="px-6 py-4 text-center">พิมพ์ 50 ทวิ (รายปี)</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 dark:divide-slate-700">
<?php foreach ($payrollData as $row): ?>
<tr class="hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors">
<td class="px-4 py-4 text-center">
<input type="checkbox" value="<?= htmlspecialchars($row['national_id']) ?>" class="tax-checkbox w-4 h-4 text-sky-600 bg-slate-100 border-slate-300 rounded focus:ring-sky-500 dark:focus:ring-sky-600 dark:ring-offset-slate-800 focus:ring-2 dark:bg-slate-700 dark:border-slate-600 cursor-pointer">
</td>
<td class="px-4 py-4">
<div class="w-10 h-10 rounded-full overflow-hidden bg-slate-200 dark:bg-slate-700 border-2 border-white dark:border-slate-600 shadow-sm flex items-center justify-center">
<img src="<?= BASE_URL ?>/employee/image/<?= urlencode($row['national_id']) ?>" alt="Profile" class="w-full h-full object-cover" onerror="this.style.display='none'">
</div>
</td>
<?php
$rawName = trim($row['hr_fname'] ?? $row['first_name']);
$prefix = '';
$bodyName = $rawName;
if (preg_match('/^(นาย|นางสาว|นาง|ว่าที่ร้อยตรี|น\.ส\.|นพ\.|พญ\.|ดร\.)\s*(.*)$/u', $rawName, $matches)) {
$prefix = $matches[1];
$bodyName = $matches[2];
}
?>
<td class="px-6 py-4 font-medium text-slate-800 dark:text-slate-200" data-sort="<?= htmlspecialchars($bodyName) ?>">
<span class="name-prefix hidden"><?= htmlspecialchars($prefix) ?></span><?= htmlspecialchars($bodyName) ?> <?= htmlspecialchars($row['hr_lname'] ?? $row['last_name']) ?>
</td>
<td class="px-6 py-4">
<div class="text-slate-800 dark:text-slate-200"><?= htmlspecialchars($row['hr_position'] ?? 'N/A') ?></div>
<div class="text-xs text-slate-500 dark:text-slate-400"><?= htmlspecialchars($row['hr_department'] ?? 'N/A') ?></div>
</td>
<td class="px-6 py-4 text-center">
<a href="<?= BASE_URL ?>/reports/slip/<?= urlencode($row['national_id']) ?>/<?= $month ?>/<?= $year ?>" target="_blank" class="inline-flex items-center px-3 py-1.5 bg-emerald-50 text-emerald-600 dark:bg-emerald-900/30 dark:text-emerald-400 border border-emerald-200 dark:border-emerald-800 rounded-lg text-xs font-medium hover:bg-emerald-100 dark:hover:bg-emerald-900/50 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6.72 13.829c-.24.03-.48.062-.72.096m.72-.096a42.415 42.415 0 0 1 10.56 0m-10.56 0L6.34 18m10.94-4.171c.24.03.48.062.72.096m-.72-.096L17.66 18m0 0 .229 2.523a1.125 1.125 0 0 1-1.12 1.227H7.231c-.662 0-1.18-.568-1.12-1.227L6.34 18m11.318 0h1.091A2.25 2.25 0 0 0 21 15.75V9.456c0-1.081-.768-2.015-1.837-2.175a48.055 48.055 0 0 0-1.913-.247M6.34 18H5.25A2.25 2.25 0 0 1 3 15.75V9.456c0-1.081.768-2.015 1.837-2.175a48.041 48.041 0 0 1 1.913-.247m10.5 0a48.536 48.536 0 0 0-10.5 0m10.5 0V3.375c0-.621-.504-1.125-1.125-1.125h-8.25c-.621 0-1.125.504-1.125 1.125v3.659M18 10.5h.008v.008H18V10.5Zm-3 0h.008v.008H15V10.5Z" /></svg>
สลิปเดือนนี้
</a>
</td>
<td class="px-6 py-4 text-center">
<a href="<?= BASE_URL ?>/reports/tax/<?= urlencode($row['national_id']) ?>/<?= $year ?>" target="_blank" class="inline-flex items-center px-3 py-1.5 bg-indigo-50 text-indigo-600 dark:bg-indigo-900/30 dark:text-indigo-400 border border-indigo-200 dark:border-indigo-800 rounded-lg text-xs font-medium hover:bg-indigo-100 dark:hover:bg-indigo-900/50 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" /></svg>
50 ทวิ (ทั้งปี)
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<!-- jQuery and DataTables JS -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.datatables.net/2.0.8/js/dataTables.js"></script>
<script src="https://cdn.datatables.net/2.0.8/js/dataTables.tailwindcss.js"></script>
<script>
$(document).ready(function() {
$('#reportTable').DataTable({
"language": {
"sProcessing": "กำลังดำเนินการ...",
"sLengthMenu": "แสดง _MENU_ เรคคอร์ด",
"sZeroRecords": "ไม่พบข้อมูล",
"sInfo": "แสดง _START_ ถึง _END_ จาก _TOTAL_ เรคคอร์ด",
"sInfoEmpty": "แสดง 0 ถึง 0 จาก 0 เรคคอร์ด",
"sInfoFiltered": "(กรองข้อมูล _MAX_ ทุกเรคคอร์ด)",
"sSearch": "ค้นหา:",
"oPaginate": {
"sFirst": "แรกสุด",
"sPrevious": "ก่อนหน้า",
"sNext": "ถัดไป",
"sLast": "สุดท้าย"
}
},
"pageLength": 50,
"columnDefs": [
{ "orderable": false, "targets": [0, 1, 4, 5] } // Disable sorting on Checkbox, Image, and Print Action columns
]
});
// Checkbox toggle prefix
$('#togglePrefix').on('change', function() {
if ($(this).is(':checked')) {
$('.name-prefix').removeClass('hidden');
} else {
$('.name-prefix').addClass('hidden');
}
});
// Handle Select All Checkbox
$('#selectAllCheckbox').on('click', function() {
var isChecked = this.checked;
// Get all rows in DataTables (including those on other pages)
var tableNodes = $('#reportTable').DataTable().rows().nodes();
$('input.tax-checkbox', tableNodes).prop('checked', isChecked);
});
// Handle Bulk Print Button
$('#bulkPrintBtn').on('click', function(e) {
e.preventDefault();
var table = $('#reportTable').DataTable();
var checkedBoxes = table.$('input.tax-checkbox:checked');
if (checkedBoxes.length === 0) {
alert('กรุณาเลือกรายชื่ออย่างน้อย 1 รายการเพื่อพิมพ์');
return;
}
// Set group_by_letter value
$('#formGroupByLetter').val($('#groupByLetter').is(':checked') ? '1' : '0');
// Clear old hidden inputs in form
$('#bulkTaxFormContainer').empty();
// Create hidden inputs for each selected ID
checkedBoxes.each(function() {
$('#bulkTaxFormContainer').append($('<input>').attr({
type: 'hidden',
name: 'national_ids[]',
value: $(this).val()
}));
});
$('#bulkTaxForm').submit();
});
});
</script>
<?php require_once APP_ROOT . '/app/Views/layouts/footer.php'; ?>
@@ -0,0 +1,145 @@
<?php require_once APP_ROOT . '/app/Views/layouts/header.php'; ?>
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-6 gap-4">
<div>
<h2 class="text-2xl font-bold text-slate-800 dark:text-white">รายงานเงินเดือน</h2>
<p class="text-slate-500 dark:text-slate-400 text-sm mt-1">แสดงข้อมูลรายได้และรายจ่ายของเจ้าหน้าที่ในแต่ละเดือน</p>
</div>
</div>
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-100 dark:border-slate-700 overflow-hidden mb-6 p-6">
<form method="GET" action="<?= BASE_URL ?>/reports/salary" class="flex flex-col md:flex-row gap-4 items-end">
<div class="w-full md:w-48">
<label for="month" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">เดือน</label>
<select id="month" name="month" class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-sky-500 bg-slate-50 dark:bg-slate-900 sm:text-sm">
<?php
$months = [
'01' => 'มกราคม', '02' => 'กุมภาพันธ์', '03' => 'มีนาคม', '04' => 'เมษายน',
'05' => 'พฤษภาคม', '06' => 'มิถุนายน', '07' => 'กรกฎาคม', '08' => 'สิงหาคม',
'09' => 'กันยายน', '10' => 'ตุลาคม', '11' => 'พฤศจิกายน', '12' => 'ธันวาคม'
];
foreach ($months as $m => $name): ?>
<option value="<?= $m ?>" <?= ($month ?? '') == $m ? 'selected' : '' ?>><?= $name ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="w-full md:w-32">
<label for="year" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">ปี (พ.ศ.)</label>
<select id="year" name="year" class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-sky-500 bg-slate-50 dark:bg-slate-900 sm:text-sm">
<?php
$currentYear = date('Y');
for ($y = $currentYear - 2; $y <= $currentYear + 1; $y++): ?>
<option value="<?= $y ?>" <?= ($year ?? '') == $y ? 'selected' : '' ?>><?= $y + 543 ?></option>
<?php endfor; ?>
</select>
</div>
<div class="w-full md:w-64">
<label for="type_id" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">ประเภทเจ้าหน้าที่</label>
<select id="type_id" name="type_id" class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-sky-500 bg-slate-50 dark:bg-slate-900 sm:text-sm">
<option value="">-- ทั้งหมด --</option>
<?php foreach ($officerTypes as $type): ?>
<option value="<?= $type['id'] ?>" <?= ($typeId ?? '') == $type['id'] ? 'selected' : '' ?>><?= htmlspecialchars($type['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="px-6 py-2.5 bg-slate-800 hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600 text-white text-sm font-medium rounded-xl transition-colors shadow-sm w-full md:w-auto flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" /></svg>
ดูรายงาน
</button>
<?php if (!empty($reportData)): ?>
<a href="<?= BASE_URL ?>/reports/salary/export?month=<?= urlencode($month) ?>&year=<?= urlencode($year) ?>&type_id=<?= urlencode($typeId) ?>" class="px-6 py-2.5 bg-emerald-600 hover:bg-emerald-700 text-white text-sm font-medium rounded-xl transition-colors shadow-sm w-full md:w-auto flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" /></svg>
ส่งออก Excel
</a>
<?php endif; ?>
</form>
</div>
<!-- DataTables CSS for Tailwind -->
<link rel="stylesheet" href="https://cdn.datatables.net/2.0.8/css/dataTables.tailwindcss.css">
<style>
/* DataTables Dark Mode Overrides */
.dark .dt-container { color: #cbd5e1; }
.dark .dt-length select, .dark .dt-search input {
background-color: #1e293b; border-color: #334155; color: #f8fafc; border-radius: 0.5rem; padding: 0.25rem 0.5rem;
}
.dark .dt-info, .dark .dt-paging-button { color: #94a3b8 !important; }
.dark .dt-paging-button.current { background: #38bdf8 !important; color: #0f172a !important; border-color: #38bdf8 !important; }
.dark .dt-paging-button:hover { background: #334155 !important; color: #f8fafc !important; }
table.dataTable.no-footer { border-bottom: 1px solid #334155 !important; }
</style>
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-100 dark:border-slate-700 overflow-hidden p-6">
<div class="overflow-x-auto">
<table id="reportTable" class="w-full text-left text-sm text-slate-600 dark:text-slate-300 display whitespace-nowrap">
<thead class="bg-slate-50 dark:bg-slate-900/50 text-slate-500 dark:text-slate-400 text-xs uppercase font-semibold">
<tr>
<th class="px-4 py-3">ลำดับ</th>
<th class="px-4 py-3">ชื่อ-สกุล</th>
<th class="px-4 py-3 text-right">เงินเดือนหลัก</th>
<th class="px-4 py-3 text-right text-emerald-600 dark:text-emerald-400">รวมรายรับ</th>
<th class="px-4 py-3 text-right text-red-600 dark:text-red-400">รวมรายจ่าย</th>
<th class="px-4 py-3 text-right text-sky-600 dark:text-sky-400">รับสุทธิ</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 dark:divide-slate-700">
<?php if (!empty($reportData)): ?>
<?php $i = 1; foreach ($reportData as $row): ?>
<tr class="hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors">
<td class="px-4 py-3 text-center"><?= $i++ ?></td>
<td class="px-4 py-3 font-medium text-slate-800 dark:text-slate-200">
<?= htmlspecialchars(trim($row['hr_prefix'] ?? '') . trim($row['hr_fname'] ?? '') . ' ' . trim($row['hr_lname'] ?? '')) ?>
</td>
<td class="px-4 py-3 text-right font-mono"><?= number_format($row['base_salary'] ?? 0, 2) ?></td>
<td class="px-4 py-3 text-right font-mono text-emerald-600 dark:text-emerald-400 font-medium"><?= number_format($row['total_income'] ?? 0, 2) ?></td>
<td class="px-4 py-3 text-right font-mono text-red-600 dark:text-red-400 font-medium"><?= number_format($row['total_deduction'] ?? 0, 2) ?></td>
<td class="px-4 py-3 text-right font-mono text-sky-600 dark:text-sky-400 font-bold"><?= number_format($row['net_salary'] ?? 0, 2) ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<!-- DataTables JS -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.datatables.net/2.0.8/js/dataTables.js"></script>
<script src="https://cdn.datatables.net/2.0.8/js/dataTables.tailwindcss.js"></script>
<script>
$(document).ready(function() {
var t = $('#reportTable').DataTable({
"language": {
"lengthMenu": "แสดง _MENU_ รายการต่อหน้า",
"zeroRecords": "ไม่พบข้อมูล",
"info": "แสดงหน้าที่ _PAGE_ จาก _PAGES_",
"infoEmpty": "ไม่มีข้อมูล",
"infoFiltered": "(กรองจากทั้งหมด _MAX_ รายการ)",
"search": "ค้นหา:",
"paginate": {
"first": "หน้าแรก",
"last": "หน้าสุดท้าย",
"next": "ถัดไป",
"previous": "ก่อนหน้า"
}
},
"pageLength": 10,
"columnDefs": [{
"searchable": false,
"orderable": false,
"targets": 0
}],
"order": [[ 1, "asc" ]] // Sort by Name by default
});
t.on('order.dt search.dt', function () {
let i = 1;
t.cells(null, 0, { search: 'applied', order: 'applied' }).every(function (cell) {
this.data(i++);
});
}).draw();
});
</script>
<?php require_once APP_ROOT . '/app/Views/layouts/footer.php'; ?>
@@ -0,0 +1,290 @@
<?php
$thaiMonths = [
'01' => 'มกราคม', '02' => 'กุมภาพันธ์', '03' => 'มีนาคม', '04' => 'เมษายน',
'05' => 'พฤษภาคม', '06' => 'มิถุนายน', '07' => 'กรกฎาคม', '08' => 'สิงหาคม',
'09' => 'กันยายน', '10' => 'ตุลาคม', '11' => 'พฤศจิกายน', '12' => 'ธันวาคม'
];
$thaiYear = $year + 543;
$monthName = $thaiMonths[$month] ?? '';
// Grouping by Fund Source
$cgdIncomes = [];
$hospIncomes = [];
foreach($incomes as $inc) {
if ($inc['amount'] > 0) {
if (($inc['fund_source'] ?? '') === 'CGD') $cgdIncomes[] = $inc;
else $hospIncomes[] = $inc;
}
}
$cgdDeductions = [];
$hospDeductions = [];
foreach($deductions as $ded) {
if ($ded['amount'] > 0) {
if (($ded['fund_source'] ?? '') === 'CGD') $cgdDeductions[] = $ded;
else $hospDeductions[] = $ded;
}
}
$cgdTotalIncome = array_sum(array_column($cgdIncomes, 'amount'));
$cgdTotalDeduction = array_sum(array_column($cgdDeductions, 'amount'));
$hospTotalIncome = array_sum(array_column($hospIncomes, 'amount'));
$hospTotalDeduction = array_sum(array_column($hospDeductions, 'amount'));
$netSalary = ($cgdTotalIncome + $hospTotalIncome) - ($cgdTotalDeduction + $hospTotalDeduction);
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>สลิปเงินเดือน - <?= htmlspecialchars($hrData['HR_FNAME'] ?? '') ?></title>
<!-- Google Fonts: Sarabun (Standard Thai Font) -->
<link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<!-- Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
sans: ['Sarabun', 'sans-serif'],
}
}
}
}
</script>
<style>
@media print {
body { background: white; margin: 0; padding: 0; }
.no-print { display: none !important; }
@page { size: A4; margin: 0; } /* Set A4 without margins */
* { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }
.slip-container {
padding: 1cm !important;
box-shadow: none !important;
max-width: 100% !important;
margin: 0 !important;
width: 210mm !important;
height: 297mm !important;
box-sizing: border-box !important;
overflow: hidden !important;
page-break-after: avoid !important;
page-break-inside: avoid !important;
border: none !important;
}
}
body { font-size: <?= htmlspecialchars($slipFontSize ?? '11pt') ?>; color: #1e293b; background-color: #f8fafc; font-family: 'Sarabun', sans-serif; }
.slip-container {
width: 210mm;
min-height: 297mm;
box-sizing: border-box;
margin: 2cm auto;
background: white;
padding: 1cm;
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
border-radius: 0.5rem;
border: 1px solid #e2e8f0;
}
table { width: 100%; border-collapse: collapse; margin-bottom: 0.75rem; }
th, td { border: 1px solid #cbd5e1; padding: 0.2rem 0.5rem; }
.bg-gray-header { background-color: #f1f5f9 !important; }
.bg-gray-light { background-color: #f8fafc !important; }
.bg-gray-dark { background-color: #e2e8f0 !important; }
.bg-white { background-color: #ffffff !important; }
.border-none { border: none !important; }
.border-l { border-left: 1px solid #cbd5e1 !important; }
.border-r { border-right: 1px solid #cbd5e1 !important; }
.border-t { border-top: 1px solid #cbd5e1 !important; }
.border-b { border-bottom: 1px solid #cbd5e1 !important; }
.w-col-name { width: 35%; }
.w-col-amt { width: 15%; }
.table-rounded { border-radius: 0.5rem; overflow: hidden; border: 1px solid #cbd5e1; }
.table-rounded table { margin-bottom: 0; }
.table-rounded th, .table-rounded td { border-color: #e2e8f0; }
.table-rounded tr:last-child td { border-bottom: none; }
</style>
</head>
<body>
<div class="fixed top-4 right-4 no-print flex space-x-2">
<button onclick="window.close()" class="px-4 py-2 bg-slate-200 hover:bg-slate-300 rounded-lg text-sm font-medium">ปิด</button>
<button onclick="window.print()" class="px-4 py-2 bg-sky-600 hover:bg-sky-700 text-white rounded-lg text-sm font-medium shadow flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M6.72 13.829c-.24.03-.48.062-.72.096m.72-.096a42.415 42.415 0 0 1 10.56 0m-10.56 0L6.34 18m10.94-4.171c.24.03.48.062.72.096m-.72-.096L17.66 18m0 0 .229 2.523a1.125 1.125 0 0 1-1.12 1.227H7.231c-.662 0-1.18-.568-1.12-1.227L6.34 18m11.318 0h1.091A2.25 2.25 0 0 0 21 15.75V9.456c0-1.081-.768-2.015-1.837-2.175a48.055 48.055 0 0 0-1.913-.247M6.34 18H5.25A2.25 2.25 0 0 1 3 15.75V9.456c0-1.081.768-2.015 1.837-2.175a48.041 48.041 0 0 1 1.913-.247m10.5 0a48.536 48.536 0 0 0-10.5 0m10.5 0V3.375c0-.621-.504-1.125-1.125-1.125h-8.25c-.621 0-1.125.504-1.125 1.125v3.659M18 10.5h.008v.008H18V10.5Zm-3 0h.008v.008H15V10.5Z" /></svg>
พิมพ์
</button>
</div>
<div class="slip-container">
<!-- Header Info Table -->
<table>
<tr>
<td colspan="4" class="bg-gray-header p-6 text-center relative border-b-0 rounded-t-lg">
<img src="<?= BASE_URL ?>/public/img/logo.png" alt="Logo" class="h-20 absolute top-4 left-1/2 transform -translate-x-1/2" onerror="this.style.display='none'">
<h1 class="text-3xl font-bold mt-20 text-slate-800">โรงพยาบาลเกาะสมุย</h1>
</td>
</tr>
<tr>
<td class="bg-gray-light font-bold w-[20%]">ชื่อ - สกุล</td>
<td class="w-[30%]"><?= htmlspecialchars($hrData['HR_FNAME'] ?? $salary['first_name']) ?> <?= htmlspecialchars($hrData['HR_LNAME'] ?? $salary['last_name']) ?></td>
<td class="bg-gray-light font-bold w-[20%]">ตำแหน่ง</td>
<td class="w-[30%]"><?= htmlspecialchars($hrData['HR_POSITION_NAME'] ?? '-') ?></td>
</tr>
<tr>
<td class="bg-gray-light font-bold">กรม</td>
<td>สำนักงานปลัดกระทรวงสาธารณสุข</td>
<td class="bg-gray-light font-bold">ระดับ</td>
<td><?= htmlspecialchars($hrData['HR_LEVEL_NAME'] ?? 'ชำนาญการ') ?></td>
</tr>
<tr>
<td class="bg-gray-light font-bold">สำนัก/กอง/ศูนย์</td>
<td>โรงพยาบาลทั่วไปเกาะสมุย</td>
<td class="bg-gray-light font-bold">เลขที่ตำแหน่ง</td>
<td><?= htmlspecialchars($hrData['HR_POSITION_NUM'] ?? '-') ?></td>
</tr>
<tr>
<td class="bg-gray-light font-bold">รอบจ่าย</td>
<td>เดือน <?= $monthName ?> ปี <?= $thaiYear ?></td>
<td class="bg-gray-light font-bold">แผนก</td>
<td><?= htmlspecialchars($hrData['HR_DEPARTMENT_SUB_SUB_NAME'] ?? '-') ?></td>
</tr>
<tr>
<td class="bg-gray-light font-bold">โอนเงินเข้า</td>
<td class="w-[30%]"><?= htmlspecialchars($bankProfile['bank_name'] ?? 'ธนาคารกรุงไทย จำกัด(มหาชน)') ?></td>
<td class="bg-gray-light font-bold w-[20%]">เลขที่บัญชี</td>
<td class="w-[30%]"><?= htmlspecialchars($bankProfile['bank_account'] ?? '-') ?></td>
</tr>
</table>
<?php if (!empty($cgdIncomes) || !empty($cgdDeductions)): ?>
<!-- Section 1: กรมบัญชีกลาง -->
<table class="mb-4">
<tr>
<td colspan="4" class="bg-gray-dark font-bold text-lg p-3 text-slate-800 border-x-0 border-t-0">รายการจากกรมบัญชีกลาง</td>
</tr>
<tr class="bg-gray-light text-center font-bold">
<td class="w-col-name">รายรับ</td>
<td class="w-col-amt">จำนวน (บาท)</td>
<td class="w-col-name">รายจ่าย</td>
<td class="w-col-amt">จำนวน (บาท)</td>
</tr>
<?php
$maxCgdRows = max(count($cgdIncomes), count($cgdDeductions), 1);
for($i=0; $i<$maxCgdRows; $i++):
$inc = $cgdIncomes[$i] ?? null;
$ded = $cgdDeductions[$i] ?? null;
?>
<tr>
<td class="border-t-0 border-b-0 border-l border-r"><?= $inc ? htmlspecialchars($inc['name']) : '&nbsp;' ?></td>
<td class="border-t-0 border-b-0 border-l border-r text-right"><?= $inc ? number_format($inc['amount'], 2) : '' ?></td>
<td class="border-t-0 border-b-0 border-l border-r"><?= $ded ? htmlspecialchars($ded['name']) : '' ?></td>
<td class="border-t-0 border-b-0 border-l border-r text-right"><?= $ded ? number_format($ded['amount'], 2) : '' ?></td>
</tr>
<?php endfor; ?>
<tr>
<td class="border-t border-b border-l border-r font-bold text-center bg-gray-light text-slate-700">รวมรับ</td>
<td class="border-t border-b border-l border-r text-right font-medium"><?= number_format($cgdTotalIncome, 2) ?></td>
<td class="border-t border-b border-l border-r font-bold text-center bg-gray-light text-slate-700">รวมจ่าย</td>
<td class="border-t border-b border-l border-r text-right font-medium"><?= number_format($cgdTotalDeduction, 2) ?></td>
</tr>
<tr>
<td colspan="2" class="border-t border-b border-l border-r bg-white" style="border-right: none;"></td>
<td class="border-t border-b border-l border-r font-bold text-center bg-gray-dark text-slate-800">รับสุทธิจากกรมบัญชีกลาง</td>
<td class="border-t border-b border-l border-r text-right bg-white font-bold text-sky-700"><?= number_format($cgdTotalIncome - $cgdTotalDeduction, 2) ?></td>
</tr>
</table>
<?php endif; ?>
<?php if (!empty($hospIncomes) || !empty($hospDeductions)): ?>
<!-- Section 2: โรงพยาบาล -->
<table class="mb-4">
<tr>
<td colspan="4" class="bg-gray-dark font-bold text-lg p-3 text-slate-800 border-x-0 border-t-0">รายการจากโรงพยาบาล</td>
</tr>
<tr class="bg-gray-light text-center font-bold">
<td class="w-col-name">รายรับ</td>
<td class="w-col-amt">จำนวน (บาท)</td>
<td class="w-col-name">รายจ่าย</td>
<td class="w-col-amt">จำนวน (บาท)</td>
</tr>
<?php
$maxHospRows = max(count($hospIncomes), count($hospDeductions), 1);
for($i=0; $i<$maxHospRows; $i++):
$inc = $hospIncomes[$i] ?? null;
$ded = $hospDeductions[$i] ?? null;
?>
<tr>
<td class="border-t-0 border-b-0 border-l border-r"><?= $inc ? htmlspecialchars($inc['name']) : '&nbsp;' ?></td>
<td class="border-t-0 border-b-0 border-l border-r text-right"><?= $inc ? number_format($inc['amount'], 2) : '' ?></td>
<td class="border-t-0 border-b-0 border-l border-r"><?= $ded ? htmlspecialchars($ded['name']) : '' ?></td>
<td class="border-t-0 border-b-0 border-l border-r text-right"><?= $ded ? number_format($ded['amount'], 2) : '' ?></td>
</tr>
<?php endfor; ?>
<tr>
<td class="border-t border-b border-l border-r font-bold text-center bg-gray-light text-slate-700">รวมรับ</td>
<td class="border-t border-b border-l border-r text-right font-medium"><?= number_format($hospTotalIncome, 2) ?></td>
<td class="border-t border-b border-l border-r font-bold text-center bg-gray-light text-slate-700">รวมจ่าย</td>
<td class="border-t border-b border-l border-r text-right font-medium"><?= number_format($hospTotalDeduction, 2) ?></td>
</tr>
<tr>
<td colspan="2" class="border-t border-b border-l border-r bg-white" style="border-right: none;"></td>
<td class="border-t border-b border-l border-r font-bold text-center bg-gray-dark text-slate-800">รับสุทธิจากโรงพยาบาล</td>
<td class="border-t border-b border-l border-r text-right bg-white font-bold text-sky-700"><?= number_format($hospTotalIncome - $hospTotalDeduction, 2) ?></td>
</tr>
</table>
<?php endif; ?>
<!-- Grand Totals -->
<table class="mb-4">
<tr>
<td class="bg-gray-dark font-bold text-center text-xl p-4 w-[85%] text-slate-800 rounded-l-lg border-r-0">รวมรับสุทธิทั้งสิ้น</td>
<td class="text-right font-bold text-xl p-4 w-[15%] bg-white text-emerald-600 rounded-r-lg border-l-0"><?= number_format($netSalary, 2) ?></td>
</tr>
</table>
<!-- Signature -->
<div class="mt-16 flex justify-end pr-8">
<table class="text-center w-72 border-none">
<tr class="border-none">
<td class="text-right w-16 pr-2 border-none pb-1 align-bottom text-slate-700">ลงชื่อ</td>
<td class="border-b border-dotted border-slate-300 border-t-0 border-l-0 border-r-0 h-16 align-bottom pb-1 relative">
<?php
$sigPath = APP_ROOT . '/public/img/signature.png';
if (file_exists($sigPath)):
$modTime = filemtime($sigPath);
// signatureScale from Controller (default 100)
$scale = $signatureScale ?? 100;
$maxHeight = 3 * ($scale / 100) . 'rem'; // 3rem is roughly 48px (h-12)
// Signature position shift (default 0)
$posX = $signaturePosX ?? '0';
$posY = $signaturePosY ?? '0';
?>
<img src="<?= BASE_URL ?>/public/img/signature.png?t=<?= $modTime ?>" alt="Signature" class="mx-auto object-contain mix-blend-multiply absolute bottom-1 left-0 right-0" style="max-height: <?= $maxHeight ?>; transform: translate(<?= $posX ?>px, <?= $posY ?>px);" onerror="this.style.display='none'">
<?php endif; ?>
</td>
</tr>
<tr class="border-none">
<td class="border-none"></td>
<td class="pt-4 border-none"><?= htmlspecialchars($signatureName ?? '( นางณัฐฐิณี เรืองทอง )') ?></td>
</tr>
<tr class="border-none">
<td class="border-none"></td>
<td class="border-none"><?= htmlspecialchars($signaturePosition ?? 'นักวิชาการเงินและบัญชี') ?></td>
</tr>
</table>
</div>
</div>
</body>
</html>
@@ -0,0 +1,515 @@
<?php
$thaiYear = $year + 543;
$totalIncome = $yearlySummary['total_income'] ?? 0;
$totalTaxable = $yearlySummary['total_taxable_income'] ?? 0;
$totalTax = $yearlySummary['total_tax'] ?? 0;
$totalSsf = $yearlySummary['total_ssf'] ?? 0;
$totalKhbk = $yearlySummary['total_khbk'] ?? 0; // กบข.
if ($totalTaxable == 0 && $totalIncome > 0) {
$totalTaxable = $totalIncome;
}
// Format numbers
$fmtTaxable = $totalTaxable > 0 ? number_format($totalTaxable, 2) : '-';
$fmtTax = $totalTax > 0 ? number_format($totalTax, 2) : '-';
// Signature logic
$hasSignature = !empty($taxSignatureName) || file_exists(APP_ROOT . '/public/img/tax_signature.png');
$sigScale = isset($taxSignatureScale) ? (int)$taxSignatureScale / 100 : 1.0;
$sigPosX = isset($taxSignaturePosX) ? (int)$taxSignaturePosX : 0;
$sigPosY = isset($taxSignaturePosY) ? (int)$taxSignaturePosY : 0;
$sigWidth = 150 * $sigScale;
// Print Date logic
$thaiMonths = ['', 'มกราคม', 'กุมภาพันธ์', 'มีนาคม', 'เมษายน', 'พฤษภาคม', 'มิถุนายน', 'กรกฎาคม', 'สิงหาคม', 'กันยายน', 'ตุลาคม', 'พฤศจิกายน', 'ธันวาคม'];
$printDay = date('j');
$printMonth = $thaiMonths[date('n')];
$printYear = date('Y') + 543;
// Thai Baht text conversion function
function bahtText($amount) {
if ($amount == 0) return 'ศูนย์บาทถ้วน';
$amount = number_format($amount, 2, '.', '');
$number = explode('.', $amount);
$text = '';
$numberText = ['ศูนย์', 'หนึ่ง', 'สอง', 'สาม', 'สี่', 'ห้า', 'หก', 'เจ็ด', 'แปด', 'เก้า'];
$positionText = ['', 'สิบ', 'ร้อย', 'พัน', 'หมื่น', 'แสน', 'ล้าน'];
foreach ($number as $index => $part) {
$partText = '';
$part = (string)(int)$part;
if ($part === '0') continue;
$length = strlen($part);
for ($i = 0; $i < $length; $i++) {
$digit = (int)$part[$i];
$position = $length - $i - 1;
if ($digit !== 0) {
if ($position === 1 && $digit === 1) {
$partText .= $positionText[$position];
} elseif ($position === 1 && $digit === 2) {
$partText .= 'ยี่' . $positionText[$position];
} elseif ($position === 0 && $digit === 1 && $length > 1) {
$partText .= 'เอ็ด';
} else {
$partText .= $numberText[$digit] . $positionText[$position];
}
}
}
if ($index === 0) {
$text .= $partText . 'บาท';
} elseif ($index === 1) {
$text .= $partText . 'สตางค์';
}
}
if (count($number) == 1 || (int)$number[1] == 0) {
$text .= 'ถ้วน';
}
return $text;
}
$fmtTaxableStr = bahtText($totalTaxable);
$fmtTaxStr = bahtText($totalTax);
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>หนังสือรับรองการหักภาษี ณ ที่จ่าย (50 ทวิ) - <?= htmlspecialchars($hrData['HR_FNAME'] ?? '') ?></title>
<link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: { sans: ['Sarabun', 'sans-serif'] }
}
}
}
</script>
<style>
body { font-size: <?= htmlspecialchars($taxFontSize ?? '11pt') ?>; color: #000; background-color: #525659; }
.page {
background: white;
width: 210mm;
min-height: 297mm;
margin: 0 auto;
padding: 10mm;
box-shadow: 0 0 10px rgba(0,0,0,0.5);
margin-bottom: 2cm;
position: relative;
box-sizing: border-box;
}
.form-border { border: 1px solid #000; }
.form-border-t { border-top: 1px solid #000; }
.form-border-b { border-bottom: 1px solid #000; }
.form-border-l { border-left: 1px solid #000; }
.form-border-r { border-right: 1px solid #000; }
.checkbox { width: 14px; height: 14px; border: 1px solid #000; display: inline-block; vertical-align: middle; position: relative; margin-right: 4px; margin-left: 8px; }
.checkbox:first-child { margin-left: 0; }
.checkbox.checked::after { content: '✓'; position: absolute; top: -3px; left: 1px; font-weight: bold; font-size: 12px; color: #000; }
table.data-table { width: 100%; border-collapse: collapse; }
table.data-table th, table.data-table td { border: 1px solid #a3a3a3; padding: 1px 4px; vertical-align: middle; }
table.data-table tr:last-child td { border-bottom: none; }
/* Compact line height for strict fitting */
.compact-text { line-height: 1.1; }
.small-text { font-size: 0.8em; }
/* Char boxes */
.char-box { display: inline-flex; }
.char-box span { width: 14px; height: 20px; border: 1px solid #000; display: inline-flex; justify-content: center; align-items: center; font-size: 10pt; margin-right: 2px; }
.char-box .gap { border: none !important; width: 6px; margin-right: 0; }
/* Dotted lines */
.border-dotted { border-color: #aaa !important; border-width: 1px !important; }
@media print {
body {
background: white;
margin: 0;
padding: 0;
zoom: 95%; /* ปรับ scale ลง 5% สำหรับ Chrome/Edge ให้พอดีหน้า */
}
.page {
width: 100%;
margin: 0;
padding: 5mm 8mm; /* ลดขอบกระดาษตอนพิมพ์ลงเพื่อเพิ่มพื้นที่แนวตั้ง */
box-shadow: none;
border: none;
page-break-inside: avoid;
page-break-after: always;
}
.no-print { display: none !important; }
@page { size: A4; margin: 0; }
}
</style>
</head>
<body class="py-4 font-sans print:py-0">
<div class="fixed top-4 right-4 no-print flex space-x-2 z-50">
<button onclick="window.close()" class="px-4 py-2 bg-slate-200 hover:bg-slate-300 rounded-lg text-sm font-medium">ปิด</button>
<button onclick="window.print()" class="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-sm font-medium shadow flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M6.72 13.829c-.24.03-.48.062-.72.096m.72-.096a42.415 42.415 0 0 1 10.56 0m-10.56 0L6.34 18m10.94-4.171c.24.03.48.062.72.096m-.72-.096L17.66 18m0 0 .229 2.523a1.125 1.125 0 0 1-1.12 1.227H7.231c-.662 0-1.18-.568-1.12-1.227L6.34 18m11.318 0h1.091A2.25 2.25 0 0 0 21 15.75V9.456c0-1.081-.768-2.015-1.837-2.175a48.055 48.055 0 0 0-1.913-.247M6.34 18H5.25A2.25 2.25 0 0 1 3 15.75V9.456c0-1.081.768-2.015 1.837-2.175a48.041 48.041 0 0 1 1.913-.247m10.5 0a48.536 48.536 0 0 0-10.5 0m10.5 0V3.375c0-.621-.504-1.125-1.125-1.125h-8.25c-.621 0-1.125.504-1.125 1.125v3.659M18 10.5h.008v.008H18V10.5Zm-3 0h.008v.008H15V10.5Z" /></svg>
พิมพ์เอกสาร
</button>
</div>
<!-- START PAGE -->
<div class="page compact-text relative">
<!-- Header -->
<div class="flex justify-between items-start mb-1">
<div class="w-1/4">
<!-- ฉบับที่ is usually inside the form box or top left, standard form has it inside -->
</div>
<div class="w-2/4 text-center">
<h1 class="text-xl font-bold mb-0">หนังสือรับรองการหักภาษี ณ ที่จ่าย</h1>
<p class="font-bold">ตามมาตรา 50 ทวิ แห่งประมวลรัษฎากร</p>
</div>
<div class="w-1/4 text-right text-[9pt] text-slate-700 pt-2">
<div class="flex justify-end items-end mb-1">
<span class="mr-1">เล่มที่</span> <span class="inline-block w-20 border-b border-dotted border-slate-400"></span>
</div>
<div class="flex justify-end items-end">
<span class="mr-1">เลขที่</span> <span class="inline-block w-20 border-b border-dotted border-slate-400"></span>
</div>
</div>
</div>
<div class="flex justify-between text-xs mb-1">
<div>
ฉบับที่ 1 (สำหรับผู้ถูกหักภาษี ณ ที่จ่าย ใช้แนบพร้อมกับแบบแสดงรายการภาษี)<br>
ฉบับที่ 2 (สำหรับผู้ถูกหักภาษี ณ ที่จ่าย เก็บไว้เป็นหลักฐาน)
</div>
</div>
<!-- Main Outer Box -->
<div class="form-border w-full">
<!-- Payer Section -->
<div class="p-1 pb-1">
<div class="flex justify-between items-center mb-1">
<b>ผู้มีหน้าที่หักภาษี ณ ที่จ่าย :</b>
<div class="flex items-center pr-2">
<span class="mr-2">เลขประจำตัวผู้เสียภาษีอากร (13 หลัก)*</span>
<div class="char-box">
<?php
$orgId = str_replace('-', '', $orgTaxId ?? '');
$orgId = str_pad($orgId, 13, ' ', STR_PAD_RIGHT);
for ($i = 0; $i < 13; $i++) {
echo "<span>" . htmlspecialchars($orgId[$i]) . "</span>";
if (in_array($i, [0, 4, 9, 11])) echo "<div class='gap'></div>";
}
?>
</div>
</div>
</div>
<div class="flex items-end mt-1">
<b class="w-10">ชื่อ</b>
<div class="border-b border-dotted border-black flex-grow text-[11pt] px-4 text-left">โรงพยาบาลเกาะสมุย</div>
</div>
<div class="small-text font-normal text-slate-600 ml-10 leading-tight">(ให้ระบุว่าเป็น บุคคล นิติบุคคล บริษัท สมาคม หรือคณะบุคคล)</div>
<div class="flex items-end mt-1">
<b class="w-10">ที่อยู่</b>
<div class="border-b border-dotted border-black flex-grow text-[11pt] px-4 text-left"><?= htmlspecialchars($orgAddress ?? 'ต.อ่างทอง อ.เกาะสมุย จ.สุราษฎร์ธานี 84140') ?></div>
</div>
<div class="small-text font-normal text-slate-600 ml-10 leading-tight mb-1">(ให้ระบุชื่ออาคาร/หมู่บ้าน ห้องเลขที่ ชั้นที่ เลขที่ ตรอก/ซอย หมู่ที่ ถนน ตำบล/แขวง อำเภอ/เขต จังหวัด)</div>
</div>
<!-- Payee Section -->
<div class="p-1 pb-1 border-t border-black">
<div class="flex justify-between items-center mb-1">
<b>ผู้ถูกหักภาษี ณ ที่จ่าย :</b>
<div class="flex items-center pr-2">
<span class="mr-2">เลขประจำตัวผู้เสียภาษีอากร (13 หลัก)*</span>
<div class="char-box">
<?php
$nid = str_replace('-', '', $nationalId ?? '');
$nid = str_pad($nid, 13, ' ', STR_PAD_RIGHT);
for ($i = 0; $i < 13; $i++) {
echo "<span>" . htmlspecialchars($nid[$i]) . "</span>";
if (in_array($i, [0, 4, 9, 11])) echo "<div class='gap'></div>";
}
?>
</div>
</div>
</div>
<div class="flex items-end mt-1">
<b class="w-10">ชื่อ</b>
<div class="border-b border-dotted border-black flex-grow text-[11pt] px-4 text-left"><?= htmlspecialchars($hrData['HR_PREFIX_NAME'] ?? '') ?><?= htmlspecialchars($hrData['HR_FNAME'] ?? '') ?> <?= htmlspecialchars($hrData['HR_LNAME'] ?? '') ?></div>
</div>
<div class="small-text font-normal text-slate-600 ml-10 leading-tight">(ให้ระบุว่าเป็น บุคคล นิติบุคคล บริษัท สมาคม หรือคณะบุคคล)</div>
<div class="flex items-end mt-1">
<b class="w-10">ที่อยู่</b>
<div class="border-b border-dotted border-black flex-grow text-[11pt] px-4 text-left"><?= htmlspecialchars($taxPayeeAddress ?? 'ต.อ่างทอง อ.เกาะสมุย จ.สุราษฎร์ธานี 84140') ?></div>
</div>
<div class="small-text font-normal text-slate-600 ml-10 leading-tight mb-1">(ให้ระบุชื่ออาคาร/หมู่บ้าน ห้องเลขที่ ชั้นที่ เลขที่ ตรอก/ซอย หมู่ที่ ถนน ตำบล/แขวง อำเภอ/เขต จังหวัด)</div>
</div>
<!-- Form Type Checkboxes -->
<div class="p-1 border-t border-black text-[10pt]">
<div class="flex items-start">
<div class="flex items-end mr-4 shrink-0 pb-1">
<b class="mr-1">ลำดับที่</b>
<span class="border-b border-dotted border-black inline-block w-12 text-center translate-y-[-2px]"></span>
<span class="ml-4">ในแบบ</span>
</div>
<div class="flex flex-wrap items-center flex-1">
<div class="flex items-center mr-4 mb-1"><span class="checkbox checked mr-1 ml-0"></span> (1) ภ.ง.ด.1ก</div>
<div class="flex items-center mr-4 mb-1"><span class="checkbox mr-1 ml-0"></span> (2) ภ.ง.ด.1ก พิเศษ</div>
<div class="flex items-center mr-4 mb-1"><span class="checkbox mr-1 ml-0"></span> (3) ภ.ง.ด.2</div>
<div class="flex items-center mr-4 mb-1"><span class="checkbox mr-1 ml-0"></span> (4) ภ.ง.ด.3</div>
<div class="flex items-center mr-4 mb-1"><span class="checkbox mr-1 ml-0"></span> (5) ภ.ง.ด.2ก</div>
<div class="flex items-center mr-4 mb-1"><span class="checkbox mr-1 ml-0"></span> (6) ภ.ง.ด.3ก</div>
<div class="flex items-center mr-4 mb-1"><span class="checkbox mr-1 ml-0"></span> (7) ภ.ง.ด.53</div>
</div>
</div>
<div class="small-text text-slate-600 mt-1 leading-tight">
(ให้สามารถอ้างอิงหรือสอบยันกันได้ระหว่างลำดับที่ตามหนังสือรับรองฯ กับแบบยื่นรายการภาษีหักที่จ่าย)
</div>
</div>
<!-- Table -->
<table class="data-table border-t border-black text-[10pt] w-full border-b-0 border-l-0 border-r-0">
<thead class="text-center font-bold align-middle bg-white">
<tr>
<th class="w-[60%] border-l-0 border-t-0 border-b-black py-2">ประเภทเงินได้พึงประเมินที่จ่าย</th>
<th class="w-[12%] border-t-0 border-b-black py-2">วัน เดือน<br>หรือปีภาษี ที่จ่าย</th>
<th class="w-[14%] border-t-0 border-b-black py-2">จำนวนเงินที่จ่าย</th>
<th class="w-[14%] border-r-0 border-t-0 border-b-black py-2">ภาษีที่หัก<br>และนำส่งไว้</th>
</tr>
</thead>
<tbody>
<tr>
<td class="border-l-0">1. เงินเดือน ค่าจ้าง เบี้ยเลี้ยง โบนัส ฯลฯ ตามมาตรา 40 (1)</td>
<td class="text-center">ปี <?= $thaiYear ?></td>
<td class="text-right"><?= $fmtTaxable ?></td>
<td class="text-right border-r-0"><?= $fmtTax ?></td>
</tr>
<tr>
<td class="border-l-0">2. ค่าธรรมเนียม ค่านายหน้า ฯลฯ ตามมาตรา 40 (2)</td>
<td class="text-center"></td>
<td class="text-right"></td>
<td class="text-right border-r-0"></td>
</tr>
<tr>
<td class="border-l-0">3. ค่าแห่งลิขสิทธิ์ ฯลฯ ตามมาตรา 40 (3)</td>
<td class="text-center"></td>
<td class="text-right"></td>
<td class="text-right border-r-0"></td>
</tr>
<tr>
<td class="border-l-0">4. (ก) ดอกเบี้ย ฯลฯ ตามมาตรา 40 (4) (ก)</td>
<td class="text-center"></td>
<td class="text-right"></td>
<td class="text-right border-r-0"></td>
</tr>
<tr>
<td class="border-l-0 pl-4">(ข) เงินปันผล เงินส่วนแบ่งกำไร ฯลฯ ตามมาตรา 40 (4) (ข)</td>
<td class="text-center"></td>
<td class="text-right"></td>
<td class="text-right border-r-0"></td>
</tr>
<tr>
<td class="border-l-0 pl-8">(1) กรณีผู้ได้รับเงินปันผลได้รับเครดิตภาษี โดยจ่ายจาก<br>
&nbsp;&nbsp;&nbsp;&nbsp;กำไรสุทธิของกิจการที่ต้องเสียภาษีเงินได้นิติบุคคลในอัตราดังนี้
</td>
<td class="text-center"></td>
<td class="text-right"></td>
<td class="text-right border-r-0"></td>
</tr>
<tr>
<td class="border-l-0 pl-10">(1.1) อัตราร้อยละ 30 ของกำไรสุทธิ</td>
<td class="text-center"></td>
<td class="text-right"></td>
<td class="text-right border-r-0"></td>
</tr>
<tr>
<td class="border-l-0 pl-10">(1.2) อัตราร้อยละ 25 ของกำไรสุทธิ</td>
<td class="text-center"></td>
<td class="text-right"></td>
<td class="text-right border-r-0"></td>
</tr>
<tr>
<td class="border-l-0 pl-10">(1.3) อัตราร้อยละ 20 ของกำไรสุทธิ</td>
<td class="text-center"></td>
<td class="text-right"></td>
<td class="text-right border-r-0"></td>
</tr>
<tr>
<td class="border-l-0 pl-10">(1.4) อัตราอื่น ๆ (ระบุ)........................... ของกำไรสุทธิ</td>
<td class="text-center"></td>
<td class="text-right"></td>
<td class="text-right border-r-0"></td>
</tr>
<tr>
<td class="border-l-0 pl-8">(2) กรณีผู้ได้รับเงินปันผลไม่ได้รับเครดิตภาษี เนื่องจากจ่ายจาก</td>
<td class="text-center"></td>
<td class="text-right"></td>
<td class="text-right border-r-0"></td>
</tr>
<tr>
<td class="border-l-0 pl-10">(2.1) กำไรสุทธิของกิจการที่ได้รับยกเว้นภาษีเงินได้นิติบุคคล</td>
<td class="text-center"></td>
<td class="text-right"></td>
<td class="text-right border-r-0"></td>
</tr>
<tr>
<td class="border-l-0 pl-10">(2.2) เงินปันผลหรือเงินส่วนแบ่งของกำไรที่ได้รับยกเว้นไม่ต้องนำมารวม<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;คำนวณเป็นรายได้เพื่อเสียภาษีเงินได้นิติบุคคล
</td>
<td class="text-center"></td>
<td class="text-right"></td>
<td class="text-right border-r-0"></td>
</tr>
<tr>
<td class="border-l-0 pl-10">(2.3) กำไรสุทธิส่วนที่ได้หักผลขาดทุนสุทธิยกมาไม่เกิน 5 ปี<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ก่อนรอบระยะเวลาบัญชีปีปัจจุบัน
</td>
<td class="text-center"></td>
<td class="text-right"></td>
<td class="text-right border-r-0"></td>
</tr>
<tr>
<td class="border-l-0 pl-10">(2.4) กำไรที่รับรู้ทางบัญชีโดยวิธีส่วนได้เสีย (equity method)</td>
<td class="text-center"></td>
<td class="text-right"></td>
<td class="text-right border-r-0"></td>
</tr>
<tr>
<td class="border-l-0 pl-10">(2.5) อื่น ๆ (ระบุ)..................................................................................</td>
<td class="text-center"></td>
<td class="text-right"></td>
<td class="text-right border-r-0"></td>
</tr>
<tr>
<td class="border-l-0">
5. การจ่ายเงินได้ที่ต้องหักภาษี ณ ที่จ่าย ตามคำสั่งกรมสรรพากรที่ออกตามมาตรา<br>
&nbsp;&nbsp;&nbsp;3 เตส เช่น รางวัล ส่วนลดหรือประโยชน์ใด ๆ เนื่องจากการส่งเสริมการขาย รางวัล<br>
&nbsp;&nbsp;&nbsp;ในการประกวด การแข่งขัน การชิงโชค ค่าแสดงของนักแสดงสาธารณะ ค่าจ้าง<br>
&nbsp;&nbsp;&nbsp;ทำของ ค่าโฆษณา ค่าเช่า ค่าขนส่ง ค่าบริการ ค่าเบี้ยประกันวินาศภัย ฯลฯ
</td>
<td class="text-center"></td>
<td class="text-right"></td>
<td class="text-right border-r-0"></td>
</tr>
<tr>
<td class="border-l-0">6. อื่น ๆ (ระบุ)....................................................................................................................</td>
<td class="text-center"></td>
<td class="text-right"></td>
<td class="text-right border-r-0"></td>
</tr>
<tr class="font-bold">
<td colspan="2" class="text-center border-l-0">รวมเงินที่จ่ายและภาษีที่หักนำส่ง</td>
<td class="text-right"><?= $fmtTaxable ?></td>
<td class="text-right border-r-0"><?= $fmtTax ?></td>
</tr>
<tr>
<td colspan="4" class="border-l-0 border-r-0 pb-1 pt-1">
รวมเงินที่จ่าย (ตัวอักษร) <span class="inline-block border-b border-dotted border-black w-[80%] text-center pl-2">&nbsp;&nbsp;( <?= htmlspecialchars($fmtTaxableStr) ?> )</span>
</td>
</tr>
<tr>
<td colspan="4" class="border-l-0 border-r-0 pb-1 pt-1 border-t-0">
รวมเงินภาษีที่หักนำส่ง (ตัวอักษร) <span class="inline-block border-b border-dotted border-black w-[80%] text-center pl-2">&nbsp;&nbsp;( <?= htmlspecialchars($fmtTaxStr) ?> )</span>
</td>
</tr>
</tbody>
</table>
<!-- Funds Section -->
<div class="p-1 border-t border-black text-[10pt] flex items-center justify-between">
<span>เงินที่จ่ายเข้า กบข./กสจ./กองทุนสงเคราะห์ครูโรงเรียนเอกชน <span class="border-b border-dotted border-black inline-block w-24 text-center"><?= number_format($totalKhbk, 2) ?></span> บาท</span>
<span>กองทุนประกันสังคม <span class="border-b border-dotted border-black inline-block w-24 text-center"><?= number_format($totalSsf, 2) ?></span> บาท</span>
<span>กองทุนสำรองเลี้ยงชีพ <span class="border-b border-dotted border-black inline-block w-24 text-center"></span> บาท</span>
</div>
<!-- Condition Section -->
<div class="p-1 border-t border-black text-[10pt] flex items-center bg-slate-100/50 whitespace-nowrap overflow-hidden">
<b class="mr-4">ผู้จ่ายเงิน</b>
<span class="checkbox checked"></span> (1) หัก ณ ที่จ่าย
<span class="checkbox ml-3"></span> (2) ออกให้ตลอดไป
<span class="checkbox ml-3"></span> (3) ออกให้ครั้งเดียว
<span class="checkbox ml-3"></span> (4) อื่น ๆ (ระบุ)<span class="inline-block border-b border-dotted w-16 ml-1"></span>
</div>
<!-- Signature Section -->
<div class="flex border-t border-black min-h-[100px]">
<!-- Warning Text -->
<div class="w-[35%] p-2 border-r border-black text-[10pt]">
<b>คำเตือน</b> ผู้มีหน้าที่ออกหนังสือรับรองการหักภาษี ณ ที่จ่าย<br>
<span class="pl-10">ฝ่าฝืนไม่ปฏิบัติตามมาตรา 50 ทวิ แห่งประมวล<br></span>
<span class="pl-10">รัษฎากร ต้องรับโทษทางอาญาตามมาตรา 35<br></span>
<span class="pl-10">แห่งประมวลรัษฎากร</span>
</div>
<!-- Signature Area -->
<div class="w-[65%] p-2 relative text-center text-[10pt]">
<p class="mb-4">ขอรับรองว่าข้อความและตัวเลขดังกล่าวข้างต้นถูกต้องตรงกับความจริงทุกประการ</p>
<?php if ($hasSignature): ?>
<div class="absolute inset-0 flex justify-center items-center pointer-events-none mt-4">
<?php if (file_exists(APP_ROOT . '/public/img/tax_signature.png')): ?>
<img src="<?= BASE_URL ?>/public/img/tax_signature.png?t=<?= time() ?>"
class="mix-blend-multiply dark:mix-blend-normal z-10"
style="width: <?= $sigWidth ?>px; transform: translate(<?= $sigPosX ?>px, <?= $sigPosY ?>px);"
alt="Signature">
<?php endif; ?>
</div>
<?php endif; ?>
<div class="flex justify-center items-end mt-8 relative z-20">
<span class="mr-2">ลงชื่อ</span>
<div class="border-b border-dotted border-black w-64 text-center"></div>
<span class="ml-2">ผู้จ่ายเงิน</span>
<!-- Seal Circle Placeholder -->
<div class="absolute right-4 bottom-0 w-16 h-16 rounded-full border border-black flex items-center justify-center text-[8pt] text-center leading-tight">
ประทับตรา<br>นิติบุคคล<br>(ถ้ามี)
</div>
</div>
<div class="mt-1 relative z-20"><span class="font-semibold text-sky-800 dark:text-sky-300 print-text-black"><?= htmlspecialchars($taxSignatureName ?: '......................................................') ?></span></div>
<div class="mt-1 flex justify-center items-center gap-1 relative z-20">
<span>(วัน เดือน ปี ที่ออกหนังสือรับรองฯ)</span>
</div>
<div class="mt-1 flex justify-center items-center gap-1 relative z-20">
<span class="w-10 border-b border-dotted border-black inline-block text-center"><?= $printDay ?></span>
<span>/</span>
<span class="w-24 border-b border-dotted border-black inline-block text-center"><?= $printMonth ?></span>
<span>/</span>
<span class="w-14 border-b border-dotted border-black inline-block text-center"><?= $printYear ?></span>
</div>
</div>
</div>
</div>
<!-- Footer Note -->
<div class="text-[9pt] mt-1">
<b>หมายเหตุ</b> เลขประจำตัวผู้เสียภาษีอากร (13 หลัก)* หมายถึง
1. กรณีบุคคลธรรมดาไทย ให้ใช้เลขประจำตัวประชาชนของกรมการปกครอง<br>
<span class="pl-[200px]">2. กรณีนิติบุคคล ให้ใช้เลขทะเบียนนิติบุคคลของกรมพัฒนาธุรกิจการค้า</span><br>
<span class="pl-[200px]">3. กรณีอื่นๆ นอกเหนือจาก 1. และ 2. ให้ใช้เลขประจำตัวผู้เสียภาษีอากร (13 หลัก) ของกรมสรรพากร</span>
</div>
</div>
</body>
</html>
@@ -0,0 +1,543 @@
<?php
$thaiYear = $year + 543;
// Signature logic
$hasSignature = !empty($taxSignatureName) || file_exists(APP_ROOT . '/public/img/tax_signature.png');
$sigScale = isset($taxSignatureScale) ? (int)$taxSignatureScale / 100 : 1.0;
$sigPosX = isset($taxSignaturePosX) ? (int)$taxSignaturePosX : 0;
$sigPosY = isset($taxSignaturePosY) ? (int)$taxSignaturePosY : 0;
$sigWidth = 150 * $sigScale;
// Print Date logic
$thaiMonths = ['', 'มกราคม', 'กุมภาพันธ์', 'มีนาคม', 'เมษายน', 'พฤษภาคม', 'มิถุนายน', 'กรกฎาคม', 'สิงหาคม', 'กันยายน', 'ตุลาคม', 'พฤศจิกายน', 'ธันวาคม'];
$printDay = date('j');
$printMonth = $thaiMonths[date('n')];
$printYear = date('Y') + 543;
// Thai Baht text conversion function
function bahtText($amount) {
if ($amount == 0) return 'ศูนย์บาทถ้วน';
$amount = number_format($amount, 2, '.', '');
$number = explode('.', $amount);
$text = '';
$numberText = ['ศูนย์', 'หนึ่ง', 'สอง', 'สาม', 'สี่', 'ห้า', 'หก', 'เจ็ด', 'แปด', 'เก้า'];
$positionText = ['', 'สิบ', 'ร้อย', 'พัน', 'หมื่น', 'แสน', 'ล้าน'];
foreach ($number as $index => $part) {
$partText = '';
$part = (string)(int)$part;
if ($part === '0') continue;
$length = strlen($part);
for ($i = 0; $i < $length; $i++) {
$digit = (int)$part[$i];
$position = $length - $i - 1;
if ($digit !== 0) {
if ($position === 1 && $digit === 1) {
$partText .= $positionText[$position];
} elseif ($position === 1 && $digit === 2) {
$partText .= 'ยี่' . $positionText[$position];
} elseif ($position === 0 && $digit === 1 && $length > 1) {
$partText .= 'เอ็ด';
} else {
$partText .= $numberText[$digit] . $positionText[$position];
}
}
}
if ($index === 0) {
$text .= $partText . 'บาท';
} elseif ($index === 1) {
$text .= $partText . 'สตางค์';
}
}
if (count($number) == 1 || (int)$number[1] == 0) {
$text .= 'ถ้วน';
}
return $text;
}
// Function was moved to earlier line
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>หนังสือรับรองการหักภาษี ณ ที่จ่าย (50 ทวิ) - กลุ่ม</title>
<link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: { sans: ['Sarabun', 'sans-serif'] }
}
}
}
</script>
<style>
body { font-size: <?= htmlspecialchars($taxFontSize ?? '11pt') ?>; color: #000; background-color: #525659; }
.page {
background: white;
width: 210mm;
min-height: 297mm;
margin: 0 auto;
padding: 10mm;
box-shadow: 0 0 10px rgba(0,0,0,0.5);
margin-bottom: 2cm;
position: relative;
box-sizing: border-box;
}
.form-border { border: 1px solid #000; }
.form-border-t { border-top: 1px solid #000; }
.form-border-b { border-bottom: 1px solid #000; }
.form-border-l { border-left: 1px solid #000; }
.form-border-r { border-right: 1px solid #000; }
.checkbox { width: 14px; height: 14px; border: 1px solid #000; display: inline-block; vertical-align: middle; position: relative; margin-right: 4px; margin-left: 8px; }
.checkbox:first-child { margin-left: 0; }
.checkbox.checked::after { content: '✓'; position: absolute; top: -3px; left: 1px; font-weight: bold; font-size: 12px; color: #000; }
table.data-table { width: 100%; border-collapse: collapse; }
table.data-table th, table.data-table td { border: 1px solid #a3a3a3; padding: 1px 4px; vertical-align: middle; }
table.data-table tr:last-child td { border-bottom: none; }
/* Compact line height for strict fitting */
.compact-text { line-height: 1.1; }
.small-text { font-size: 0.8em; }
/* Char boxes */
.char-box { display: inline-flex; }
.char-box span { width: 14px; height: 20px; border: 1px solid #000; display: inline-flex; justify-content: center; align-items: center; font-size: 10pt; margin-right: 2px; }
.char-box .gap { border: none !important; width: 6px; margin-right: 0; }
/* Dotted lines */
.border-dotted { border-color: #aaa !important; border-width: 1px !important; }
@media print {
body {
background: white;
margin: 0;
padding: 0;
zoom: 95%; /* ปรับ scale ลง 5% สำหรับ Chrome/Edge ให้พอดีหน้า */
}
.page {
width: 100%;
margin: 0;
padding: 5mm 8mm; /* ลดขอบกระดาษตอนพิมพ์ลงเพื่อเพิ่มพื้นที่แนวตั้ง */
box-shadow: none;
border: none;
page-break-inside: avoid;
page-break-after: always;
}
.no-print { display: none !important; }
@page { size: A4; margin: 0; }
}
</style>
</head>
<body class="py-4 font-sans print:py-0">
<div class="fixed top-4 right-4 no-print flex space-x-2 z-50">
<button onclick="window.close()" class="px-4 py-2 bg-slate-200 hover:bg-slate-300 rounded-lg text-sm font-medium">ปิด</button>
<button onclick="window.print()" class="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-sm font-medium shadow flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M6.72 13.829c-.24.03-.48.062-.72.096m.72-.096a42.415 42.415 0 0 1 10.56 0m-10.56 0L6.34 18m10.94-4.171c.24.03.48.062.72.096m-.72-.096L17.66 18m0 0 .229 2.523a1.125 1.125 0 0 1-1.12 1.227H7.231c-.662 0-1.18-.568-1.12-1.227L6.34 18m11.318 0h1.091A2.25 2.25 0 0 0 21 15.75V9.456c0-1.081-.768-2.015-1.837-2.175a48.055 48.055 0 0 0-1.913-.247M6.34 18H5.25A2.25 2.25 0 0 1 3 15.75V9.456c0-1.081.768-2.015 1.837-2.175a48.041 48.041 0 0 1 1.913-.247m10.5 0a48.536 48.536 0 0 0-10.5 0m10.5 0V3.375c0-.621-.504-1.125-1.125-1.125h-8.25c-.621 0-1.125.504-1.125 1.125v3.659M18 10.5h.008v.008H18V10.5Zm-3 0h.008v.008H15V10.5Z" /></svg>
พิมพ์เอกสาร
</button>
</div>
<!-- START PAGE LOOP -->
<?php
$currentLetter = '';
foreach ($bulkData as $data):
$hrData = $data['hrData'];
$yearlySummary = $data['yearlySummary'];
$nationalId = $data['nationalId'];
$totalIncome = $yearlySummary['total_income'] ?? 0;
$totalTaxable = $yearlySummary['total_taxable_income'] ?? 0;
$totalTax = $yearlySummary['total_tax'] ?? 0;
$totalSsf = $yearlySummary['total_ssf'] ?? 0;
$totalKhbk = $yearlySummary['total_khbk'] ?? 0;
if ($totalTaxable == 0 && $totalIncome > 0) {
$totalTaxable = $totalIncome;
}
$fmtTaxable = $totalTaxable > 0 ? number_format($totalTaxable, 2) : '-';
$fmtTax = $totalTax > 0 ? number_format($totalTax, 2) : '-';
$fmtTaxableStr = bahtText($totalTaxable);
$fmtTaxStr = bahtText($totalTax);
// Handle Cover Page if Group By Letter is enabled
if (isset($groupByLetter) && $groupByLetter) {
$fname = trim($hrData['HR_FNAME'] ?? '');
if ($fname !== '') {
$firstLetter = mb_substr($fname, 0, 1, 'UTF-8');
if ($firstLetter !== $currentLetter) {
$currentLetter = $firstLetter;
?>
<div class="page flex items-center justify-center relative">
<div class="text-[250pt] font-bold text-slate-800 leading-none">
<?= htmlspecialchars($currentLetter) ?>
</div>
</div>
<?php
}
}
}
?>
<div class="page compact-text relative">
<!-- Header -->
<div class="flex justify-between items-start mb-1">
<div class="w-1/4">
<!-- ฉบับที่ is usually inside the form box or top left, standard form has it inside -->
</div>
<div class="w-2/4 text-center">
<h1 class="text-xl font-bold mb-0">หนังสือรับรองการหักภาษี ณ ที่จ่าย</h1>
<p class="font-bold">ตามมาตรา 50 ทวิ แห่งประมวลรัษฎากร</p>
</div>
<div class="w-1/4 text-right text-[9pt] text-slate-700 pt-2">
<div class="flex justify-end items-end mb-1">
<span class="mr-1">เล่มที่</span> <span class="inline-block w-20 border-b border-dotted border-slate-400"></span>
</div>
<div class="flex justify-end items-end">
<span class="mr-1">เลขที่</span> <span class="inline-block w-20 border-b border-dotted border-slate-400"></span>
</div>
</div>
</div>
<div class="flex justify-between text-xs mb-1">
<div>
ฉบับที่ 1 (สำหรับผู้ถูกหักภาษี ณ ที่จ่าย ใช้แนบพร้อมกับแบบแสดงรายการภาษี)<br>
ฉบับที่ 2 (สำหรับผู้ถูกหักภาษี ณ ที่จ่าย เก็บไว้เป็นหลักฐาน)
</div>
</div>
<!-- Main Outer Box -->
<div class="form-border w-full">
<!-- Payer Section -->
<div class="p-1 pb-1">
<div class="flex justify-between items-center mb-1">
<b>ผู้มีหน้าที่หักภาษี ณ ที่จ่าย :</b>
<div class="flex items-center pr-2">
<span class="mr-2">เลขประจำตัวผู้เสียภาษีอากร (13 หลัก)*</span>
<div class="char-box">
<?php
$orgId = str_replace('-', '', $orgTaxId ?? '');
$orgId = str_pad($orgId, 13, ' ', STR_PAD_RIGHT);
for ($i = 0; $i < 13; $i++) {
echo "<span>" . htmlspecialchars($orgId[$i]) . "</span>";
if (in_array($i, [0, 4, 9, 11])) echo "<div class='gap'></div>";
}
?>
</div>
</div>
</div>
<div class="flex items-end mt-1">
<b class="w-10">ชื่อ</b>
<div class="border-b border-dotted border-black flex-grow text-[11pt] px-4 text-left">โรงพยาบาลเกาะสมุย</div>
</div>
<div class="small-text font-normal text-slate-600 ml-10 leading-tight">(ให้ระบุว่าเป็น บุคคล นิติบุคคล บริษัท สมาคม หรือคณะบุคคล)</div>
<div class="flex items-end mt-1">
<b class="w-10">ที่อยู่</b>
<div class="border-b border-dotted border-black flex-grow text-[11pt] px-4 text-left"><?= htmlspecialchars($orgAddress ?? 'ต.อ่างทอง อ.เกาะสมุย จ.สุราษฎร์ธานี 84140') ?></div>
</div>
<div class="small-text font-normal text-slate-600 ml-10 leading-tight mb-1">(ให้ระบุชื่ออาคาร/หมู่บ้าน ห้องเลขที่ ชั้นที่ เลขที่ ตรอก/ซอย หมู่ที่ ถนน ตำบล/แขวง อำเภอ/เขต จังหวัด)</div>
</div>
<!-- Payee Section -->
<div class="p-1 pb-1 border-t border-black">
<div class="flex justify-between items-center mb-1">
<b>ผู้ถูกหักภาษี ณ ที่จ่าย :</b>
<div class="flex items-center pr-2">
<span class="mr-2">เลขประจำตัวผู้เสียภาษีอากร (13 หลัก)*</span>
<div class="char-box">
<?php
$nid = str_replace('-', '', $nationalId ?? '');
$nid = str_pad($nid, 13, ' ', STR_PAD_RIGHT);
for ($i = 0; $i < 13; $i++) {
echo "<span>" . htmlspecialchars($nid[$i]) . "</span>";
if (in_array($i, [0, 4, 9, 11])) echo "<div class='gap'></div>";
}
?>
</div>
</div>
</div>
<div class="flex items-end mt-1">
<b class="w-10">ชื่อ</b>
<div class="border-b border-dotted border-black flex-grow text-[11pt] px-4 text-left"><?= htmlspecialchars($hrData['HR_PREFIX_NAME'] ?? '') ?><?= htmlspecialchars($hrData['HR_FNAME'] ?? '') ?> <?= htmlspecialchars($hrData['HR_LNAME'] ?? '') ?></div>
</div>
<div class="small-text font-normal text-slate-600 ml-10 leading-tight">(ให้ระบุว่าเป็น บุคคล นิติบุคคล บริษัท สมาคม หรือคณะบุคคล)</div>
<div class="flex items-end mt-1">
<b class="w-10">ที่อยู่</b>
<div class="border-b border-dotted border-black flex-grow text-[11pt] px-4 text-left"><?= htmlspecialchars($taxPayeeAddress ?? 'ต.อ่างทอง อ.เกาะสมุย จ.สุราษฎร์ธานี 84140') ?></div>
</div>
<div class="small-text font-normal text-slate-600 ml-10 leading-tight mb-1">(ให้ระบุชื่ออาคาร/หมู่บ้าน ห้องเลขที่ ชั้นที่ เลขที่ ตรอก/ซอย หมู่ที่ ถนน ตำบล/แขวง อำเภอ/เขต จังหวัด)</div>
</div>
<!-- Form Type Checkboxes -->
<div class="p-1 border-t border-black text-[10pt]">
<div class="flex items-start">
<div class="flex items-end mr-4 shrink-0 pb-1">
<b class="mr-1">ลำดับที่</b>
<span class="border-b border-dotted border-black inline-block w-12 text-center translate-y-[-2px]"></span>
<span class="ml-4">ในแบบ</span>
</div>
<div class="flex flex-wrap items-center flex-1">
<div class="flex items-center mr-4 mb-1"><span class="checkbox checked mr-1 ml-0"></span> (1) ภ.ง.ด.1ก</div>
<div class="flex items-center mr-4 mb-1"><span class="checkbox mr-1 ml-0"></span> (2) ภ.ง.ด.1ก พิเศษ</div>
<div class="flex items-center mr-4 mb-1"><span class="checkbox mr-1 ml-0"></span> (3) ภ.ง.ด.2</div>
<div class="flex items-center mr-4 mb-1"><span class="checkbox mr-1 ml-0"></span> (4) ภ.ง.ด.3</div>
<div class="flex items-center mr-4 mb-1"><span class="checkbox mr-1 ml-0"></span> (5) ภ.ง.ด.2ก</div>
<div class="flex items-center mr-4 mb-1"><span class="checkbox mr-1 ml-0"></span> (6) ภ.ง.ด.3ก</div>
<div class="flex items-center mr-4 mb-1"><span class="checkbox mr-1 ml-0"></span> (7) ภ.ง.ด.53</div>
</div>
</div>
<div class="small-text text-slate-600 mt-1 leading-tight">
(ให้สามารถอ้างอิงหรือสอบยันกันได้ระหว่างลำดับที่ตามหนังสือรับรองฯ กับแบบยื่นรายการภาษีหักที่จ่าย)
</div>
</div>
<!-- Table -->
<table class="data-table border-t border-black text-[10pt] w-full border-b-0 border-l-0 border-r-0">
<thead class="text-center font-bold align-middle bg-white">
<tr>
<th class="w-[60%] border-l-0 border-t-0 border-b-black py-2">ประเภทเงินได้พึงประเมินที่จ่าย</th>
<th class="w-[12%] border-t-0 border-b-black py-2">วัน เดือน<br>หรือปีภาษี ที่จ่าย</th>
<th class="w-[14%] border-t-0 border-b-black py-2">จำนวนเงินที่จ่าย</th>
<th class="w-[14%] border-r-0 border-t-0 border-b-black py-2">ภาษีที่หัก<br>และนำส่งไว้</th>
</tr>
</thead>
<tbody>
<tr>
<td class="border-l-0">1. เงินเดือน ค่าจ้าง เบี้ยเลี้ยง โบนัส ฯลฯ ตามมาตรา 40 (1)</td>
<td class="text-center">ปี <?= $thaiYear ?></td>
<td class="text-right"><?= $fmtTaxable ?></td>
<td class="text-right border-r-0"><?= $fmtTax ?></td>
</tr>
<tr>
<td class="border-l-0">2. ค่าธรรมเนียม ค่านายหน้า ฯลฯ ตามมาตรา 40 (2)</td>
<td class="text-center"></td>
<td class="text-right"></td>
<td class="text-right border-r-0"></td>
</tr>
<tr>
<td class="border-l-0">3. ค่าแห่งลิขสิทธิ์ ฯลฯ ตามมาตรา 40 (3)</td>
<td class="text-center"></td>
<td class="text-right"></td>
<td class="text-right border-r-0"></td>
</tr>
<tr>
<td class="border-l-0">4. (ก) ดอกเบี้ย ฯลฯ ตามมาตรา 40 (4) (ก)</td>
<td class="text-center"></td>
<td class="text-right"></td>
<td class="text-right border-r-0"></td>
</tr>
<tr>
<td class="border-l-0 pl-4">(ข) เงินปันผล เงินส่วนแบ่งกำไร ฯลฯ ตามมาตรา 40 (4) (ข)</td>
<td class="text-center"></td>
<td class="text-right"></td>
<td class="text-right border-r-0"></td>
</tr>
<tr>
<td class="border-l-0 pl-8">(1) กรณีผู้ได้รับเงินปันผลได้รับเครดิตภาษี โดยจ่ายจาก<br>
&nbsp;&nbsp;&nbsp;&nbsp;กำไรสุทธิของกิจการที่ต้องเสียภาษีเงินได้นิติบุคคลในอัตราดังนี้
</td>
<td class="text-center"></td>
<td class="text-right"></td>
<td class="text-right border-r-0"></td>
</tr>
<tr>
<td class="border-l-0 pl-10">(1.1) อัตราร้อยละ 30 ของกำไรสุทธิ</td>
<td class="text-center"></td>
<td class="text-right"></td>
<td class="text-right border-r-0"></td>
</tr>
<tr>
<td class="border-l-0 pl-10">(1.2) อัตราร้อยละ 25 ของกำไรสุทธิ</td>
<td class="text-center"></td>
<td class="text-right"></td>
<td class="text-right border-r-0"></td>
</tr>
<tr>
<td class="border-l-0 pl-10">(1.3) อัตราร้อยละ 20 ของกำไรสุทธิ</td>
<td class="text-center"></td>
<td class="text-right"></td>
<td class="text-right border-r-0"></td>
</tr>
<tr>
<td class="border-l-0 pl-10">(1.4) อัตราอื่น ๆ (ระบุ)........................... ของกำไรสุทธิ</td>
<td class="text-center"></td>
<td class="text-right"></td>
<td class="text-right border-r-0"></td>
</tr>
<tr>
<td class="border-l-0 pl-8">(2) กรณีผู้ได้รับเงินปันผลไม่ได้รับเครดิตภาษี เนื่องจากจ่ายจาก</td>
<td class="text-center"></td>
<td class="text-right"></td>
<td class="text-right border-r-0"></td>
</tr>
<tr>
<td class="border-l-0 pl-10">(2.1) กำไรสุทธิของกิจการที่ได้รับยกเว้นภาษีเงินได้นิติบุคคล</td>
<td class="text-center"></td>
<td class="text-right"></td>
<td class="text-right border-r-0"></td>
</tr>
<tr>
<td class="border-l-0 pl-10">(2.2) เงินปันผลหรือเงินส่วนแบ่งของกำไรที่ได้รับยกเว้นไม่ต้องนำมารวม<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;คำนวณเป็นรายได้เพื่อเสียภาษีเงินได้นิติบุคคล
</td>
<td class="text-center"></td>
<td class="text-right"></td>
<td class="text-right border-r-0"></td>
</tr>
<tr>
<td class="border-l-0 pl-10">(2.3) กำไรสุทธิส่วนที่ได้หักผลขาดทุนสุทธิยกมาไม่เกิน 5 ปี<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ก่อนรอบระยะเวลาบัญชีปีปัจจุบัน
</td>
<td class="text-center"></td>
<td class="text-right"></td>
<td class="text-right border-r-0"></td>
</tr>
<tr>
<td class="border-l-0 pl-10">(2.4) กำไรที่รับรู้ทางบัญชีโดยวิธีส่วนได้เสีย (equity method)</td>
<td class="text-center"></td>
<td class="text-right"></td>
<td class="text-right border-r-0"></td>
</tr>
<tr>
<td class="border-l-0 pl-10">(2.5) อื่น ๆ (ระบุ)..................................................................................</td>
<td class="text-center"></td>
<td class="text-right"></td>
<td class="text-right border-r-0"></td>
</tr>
<tr>
<td class="border-l-0">
5. การจ่ายเงินได้ที่ต้องหักภาษี ณ ที่จ่าย ตามคำสั่งกรมสรรพากรที่ออกตามมาตรา<br>
&nbsp;&nbsp;&nbsp;3 เตส เช่น รางวัล ส่วนลดหรือประโยชน์ใด ๆ เนื่องจากการส่งเสริมการขาย รางวัล<br>
&nbsp;&nbsp;&nbsp;ในการประกวด การแข่งขัน การชิงโชค ค่าแสดงของนักแสดงสาธารณะ ค่าจ้าง<br>
&nbsp;&nbsp;&nbsp;ทำของ ค่าโฆษณา ค่าเช่า ค่าขนส่ง ค่าบริการ ค่าเบี้ยประกันวินาศภัย ฯลฯ
</td>
<td class="text-center"></td>
<td class="text-right"></td>
<td class="text-right border-r-0"></td>
</tr>
<tr>
<td class="border-l-0">6. อื่น ๆ (ระบุ)....................................................................................................................</td>
<td class="text-center"></td>
<td class="text-right"></td>
<td class="text-right border-r-0"></td>
</tr>
<tr class="font-bold">
<td colspan="2" class="text-center border-l-0">รวมเงินที่จ่ายและภาษีที่หักนำส่ง</td>
<td class="text-right"><?= $fmtTaxable ?></td>
<td class="text-right border-r-0"><?= $fmtTax ?></td>
</tr>
<tr>
<td colspan="4" class="border-l-0 border-r-0 pb-1 pt-1">
รวมเงินที่จ่าย (ตัวอักษร) <span class="inline-block border-b border-dotted border-black w-[80%] text-center pl-2">&nbsp;&nbsp;( <?= htmlspecialchars($fmtTaxableStr) ?> )</span>
</td>
</tr>
<tr>
<td colspan="4" class="border-l-0 border-r-0 pb-1 pt-1 border-t-0">
รวมเงินภาษีที่หักนำส่ง (ตัวอักษร) <span class="inline-block border-b border-dotted border-black w-[80%] text-center pl-2">&nbsp;&nbsp;( <?= htmlspecialchars($fmtTaxStr) ?> )</span>
</td>
</tr>
</tbody>
</table>
<!-- Funds Section -->
<div class="p-1 border-t border-black text-[10pt] flex items-center justify-between">
<span>เงินที่จ่ายเข้า กบข./กสจ./กองทุนสงเคราะห์ครูโรงเรียนเอกชน <span class="border-b border-dotted border-black inline-block w-24 text-center"><?= number_format($totalKhbk, 2) ?></span> บาท</span>
<span>กองทุนประกันสังคม <span class="border-b border-dotted border-black inline-block w-24 text-center"><?= number_format($totalSsf, 2) ?></span> บาท</span>
<span>กองทุนสำรองเลี้ยงชีพ <span class="border-b border-dotted border-black inline-block w-24 text-center"></span> บาท</span>
</div>
<!-- Condition Section -->
<div class="p-1 border-t border-black text-[10pt] flex items-center bg-slate-100/50 whitespace-nowrap overflow-hidden">
<b class="mr-4">ผู้จ่ายเงิน</b>
<span class="checkbox checked"></span> (1) หัก ณ ที่จ่าย
<span class="checkbox ml-3"></span> (2) ออกให้ตลอดไป
<span class="checkbox ml-3"></span> (3) ออกให้ครั้งเดียว
<span class="checkbox ml-3"></span> (4) อื่น ๆ (ระบุ)<span class="inline-block border-b border-dotted w-16 ml-1"></span>
</div>
<!-- Signature Section -->
<div class="flex border-t border-black min-h-[100px]">
<!-- Warning Text -->
<div class="w-[35%] p-2 border-r border-black text-[10pt]">
<b>คำเตือน</b> ผู้มีหน้าที่ออกหนังสือรับรองการหักภาษี ณ ที่จ่าย<br>
<span class="pl-10">ฝ่าฝืนไม่ปฏิบัติตามมาตรา 50 ทวิ แห่งประมวล<br></span>
<span class="pl-10">รัษฎากร ต้องรับโทษทางอาญาตามมาตรา 35<br></span>
<span class="pl-10">แห่งประมวลรัษฎากร</span>
</div>
<!-- Signature Area -->
<div class="w-[65%] p-2 relative text-center text-[10pt]">
<p class="mb-4">ขอรับรองว่าข้อความและตัวเลขดังกล่าวข้างต้นถูกต้องตรงกับความจริงทุกประการ</p>
<?php if ($hasSignature): ?>
<div class="absolute inset-0 flex justify-center items-center pointer-events-none mt-4">
<?php if (file_exists(APP_ROOT . '/public/img/tax_signature.png')): ?>
<img src="<?= BASE_URL ?>/public/img/tax_signature.png?t=<?= time() ?>"
class="mix-blend-multiply dark:mix-blend-normal z-10"
style="width: <?= $sigWidth ?>px; transform: translate(<?= $sigPosX ?>px, <?= $sigPosY ?>px);"
alt="Signature">
<?php endif; ?>
</div>
<?php endif; ?>
<div class="flex justify-center items-end mt-8 relative z-20">
<span class="mr-2">ลงชื่อ</span>
<div class="border-b border-dotted border-black w-64 text-center"></div>
<span class="ml-2">ผู้จ่ายเงิน</span>
<!-- Seal Circle Placeholder -->
<div class="absolute right-4 bottom-0 w-16 h-16 rounded-full border border-black flex items-center justify-center text-[8pt] text-center leading-tight">
ประทับตรา<br>นิติบุคคล<br>(ถ้ามี)
</div>
</div>
<div class="mt-1 relative z-20"><span class="font-semibold text-sky-800 dark:text-sky-300 print-text-black"><?= htmlspecialchars($taxSignatureName ?: '......................................................') ?></span></div>
<div class="mt-1 flex justify-center items-center gap-1 relative z-20">
<span>(วัน เดือน ปี ที่ออกหนังสือรับรองฯ)</span>
</div>
<div class="mt-1 flex justify-center items-center gap-1 relative z-20">
<span class="w-10 border-b border-dotted border-black inline-block text-center"><?= $printDay ?></span>
<span>/</span>
<span class="w-24 border-b border-dotted border-black inline-block text-center"><?= $printMonth ?></span>
<span>/</span>
<span class="w-14 border-b border-dotted border-black inline-block text-center"><?= $printYear ?></span>
</div>
</div>
</div>
</div>
<!-- Footer Note -->
<div class="text-[9pt] mt-1">
<b>หมายเหตุ</b> เลขประจำตัวผู้เสียภาษีอากร (13 หลัก)* หมายถึง
1. กรณีบุคคลธรรมดาไทย ให้ใช้เลขประจำตัวประชาชนของกรมการปกครอง<br>
<span class="pl-[200px]">2. กรณีนิติบุคคล ให้ใช้เลขทะเบียนนิติบุคคลของกรมพัฒนาธุรกิจการค้า</span><br>
<span class="pl-[200px]">3. กรณีอื่นๆ นอกเหนือจาก 1. และ 2. ให้ใช้เลขประจำตัวผู้เสียภาษีอากร (13 หลัก) ของกรมสรรพากร</span>
</div>
</div>
<?php endforeach; ?>
</body>
</html>
@@ -0,0 +1,267 @@
<?php require_once APP_ROOT . '/app/Views/layouts/header.php'; ?>
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-6 gap-4">
<div>
<h2 class="text-2xl font-bold text-slate-800 dark:text-white">รายการรายจ่าย (Deduction Categories)</h2>
<p class="text-slate-500 dark:text-slate-400 text-sm mt-1">จัดการประเภทรายจ่าย/หักลบทั้งหมดในระบบ</p>
</div>
<button onclick="document.getElementById('createDeductionModal').classList.remove('hidden')" class="inline-flex items-center justify-center px-4 py-2.5 bg-rose-500 hover:bg-rose-600 text-white text-sm font-medium rounded-xl transition-colors shadow-sm w-full md:w-auto">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /></svg>
เพิ่มรายจ่ายใหม่
</button>
</div>
<!-- DataTables CSS for Tailwind -->
<link rel="stylesheet" href="https://cdn.datatables.net/2.0.8/css/dataTables.tailwindcss.css">
<style>
/* DataTables Dark Mode Overrides */
.dark .dt-container { color: #cbd5e1; }
.dark .dt-length select, .dark .dt-search input {
background-color: #1e293b; border-color: #334155; color: #f8fafc; border-radius: 0.5rem; padding: 0.25rem 0.5rem;
}
.dark .dt-info, .dark .dt-paging-button { color: #94a3b8 !important; }
.dark .dt-paging-button.current { background: #38bdf8 !important; color: #0f172a !important; border-color: #38bdf8 !important; }
.dark .dt-paging-button:hover { background: #334155 !important; color: #f8fafc !important; }
table.dataTable.no-footer { border-bottom: 1px solid #334155 !important; }
</style>
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-100 dark:border-slate-700 overflow-hidden p-6">
<div class="overflow-x-hidden">
<table id="deductionTable" class="w-full text-left text-sm text-slate-600 dark:text-slate-300 display">
<thead class="bg-slate-50 dark:bg-slate-900/50 text-slate-500 dark:text-slate-400 text-xs uppercase font-semibold">
<tr>
<th class="px-6 py-4">รหัส</th>
<th class="px-6 py-4">ชื่อรายการรายจ่าย</th>
<th class="px-6 py-4 text-center">แหล่งที่มาเงิน</th>
<th class="px-6 py-4 text-center">ลดหย่อนภาษี</th>
<th class="px-6 py-4 text-center">สถานะ</th>
<th class="px-6 py-4 text-right">จัดการ</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 dark:divide-slate-700">
<?php if (empty($deductions)): ?>
<tr>
<td colspan="6" class="px-6 py-8 text-center text-slate-500 dark:text-slate-400">
ยังไม่มีข้อมูลรายการรายจ่าย
</td>
</tr>
<?php else: ?>
<?php foreach ($deductions as $ded): ?>
<tr class="hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors">
<td class="px-6 py-4 font-mono font-bold text-slate-700 dark:text-slate-200">
<?= htmlspecialchars($ded['code']) ?>
</td>
<td class="px-6 py-4">
<div class="font-medium text-slate-800 dark:text-slate-200"><?= htmlspecialchars($ded['name']) ?></div>
<?php if (!empty($ded['description'])): ?>
<div class="text-xs text-slate-500 dark:text-slate-400 mt-1"><?= htmlspecialchars($ded['description']) ?></div>
<?php endif; ?>
</td>
<td class="px-6 py-4 text-center font-medium text-slate-700 dark:text-slate-200">
<?= $ded['fund_source'] === 'CGD' ? 'กรมบัญชีกลาง' : 'โรงพยาบาล' ?>
</td>
<td class="px-6 py-4 text-center">
<?php if ($ded['is_tax_deductible']): ?>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400">ใช่</span>
<?php else: ?>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-slate-100 text-slate-700 dark:bg-slate-700 dark:text-slate-300">ไม่</span>
<?php endif; ?>
</td>
<td class="px-6 py-4 text-center">
<?php if ($ded['is_active']): ?>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400">ใช้งาน</span>
<?php else: ?>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400">ปิดใช้งาน</span>
<?php endif; ?>
</td>
<td class="px-6 py-4 text-right">
<button onclick='openEditDeductionModal(<?= json_encode($ded) ?>)' class="inline-flex items-center px-3 py-1.5 border border-slate-300 dark:border-slate-600 rounded-lg text-xs font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors">
แก้ไข
</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<!-- Create Modal -->
<div id="createDeductionModal" class="hidden fixed inset-0 z-50 overflow-y-auto">
<div class="fixed inset-0 bg-slate-900/50 backdrop-blur-sm transition-opacity" onclick="document.getElementById('createDeductionModal').classList.add('hidden')"></div>
<div class="flex min-h-screen items-center justify-center p-4">
<div class="relative transform overflow-hidden rounded-2xl bg-white dark:bg-slate-800 text-left shadow-xl transition-all sm:w-full sm:max-w-lg border border-slate-100 dark:border-slate-700">
<form action="<?= BASE_URL ?>/settings/deduction" method="POST">
<div class="p-6">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-6">เพิ่มรายการรายจ่ายใหม่</h3>
<div class="space-y-4">
<div>
<label for="code" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">รหัสรายจ่าย (Code)</label>
<input type="text" id="code" name="code" required class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-rose-500 bg-slate-50 dark:bg-slate-900 sm:text-sm">
</div>
<div>
<label for="name" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">ชื่อรายการ (Name)</label>
<input type="text" id="name" name="name" required class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-rose-500 bg-slate-50 dark:bg-slate-900 sm:text-sm">
</div>
<div>
<label for="description" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">รายละเอียด (Description)</label>
<input type="text" id="description" name="description" class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-sky-500 bg-slate-50 dark:bg-slate-900 sm:text-sm">
</div>
<div>
<label for="fund_source" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">แหล่งที่มาเงิน</label>
<select id="fund_source" name="fund_source" class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-sky-500 bg-slate-50 dark:bg-slate-900 sm:text-sm">
<option value="HOSPITAL">โรงพยาบาล</option>
<option value="CGD">กรมบัญชีกลาง (CGD)</option>
</select>
</div>
<div class="flex items-center justify-between mt-4">
<label class="flex items-center cursor-pointer">
<div class="relative">
<input type="checkbox" name="is_tax_deductible" class="sr-only">
<div class="block bg-slate-200 dark:bg-slate-700 w-10 h-6 rounded-full transition-colors"></div>
<div class="dot absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform transform translate-x-full"></div>
</div>
<div class="ml-3 text-sm font-medium text-slate-700 dark:text-slate-300">ใช้ลดหย่อนภาษีได้</div>
</label>
<label class="flex items-center cursor-pointer">
<div class="relative">
<input type="checkbox" name="is_active" class="sr-only" checked>
<div class="block bg-slate-200 dark:bg-slate-700 w-10 h-6 rounded-full transition-colors"></div>
<div class="dot absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform transform translate-x-full"></div>
</div>
<div class="ml-3 text-sm font-medium text-slate-700 dark:text-slate-300">เปิดใช้งาน</div>
</label>
</div>
</div>
</div>
<div class="bg-slate-50 dark:bg-slate-700/50 px-6 py-4 flex justify-end space-x-3">
<button type="button" onclick="document.getElementById('createDeductionModal').classList.add('hidden')" class="px-4 py-2 text-sm font-semibold text-slate-700 dark:text-slate-300 bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-600 rounded-xl hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors">ยกเลิก</button>
<button type="submit" class="px-4 py-2 text-sm font-semibold text-white bg-rose-500 rounded-xl hover:bg-rose-600 transition-colors">บันทึก</button>
</div>
</form>
</div>
</div>
</div>
<!-- Edit Modal -->
<div id="editDeductionModal" class="hidden fixed inset-0 z-50 overflow-y-auto">
<div class="fixed inset-0 bg-slate-900/50 backdrop-blur-sm transition-opacity" onclick="document.getElementById('editDeductionModal').classList.add('hidden')"></div>
<div class="flex min-h-screen items-center justify-center p-4">
<div class="relative transform overflow-hidden rounded-2xl bg-white dark:bg-slate-800 text-left shadow-xl transition-all sm:w-full sm:max-w-lg border border-slate-100 dark:border-slate-700">
<form action="<?= BASE_URL ?>/settings/deduction/update" method="POST">
<input type="hidden" id="edit_id" name="id">
<div class="p-6">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-6">แก้ไขรายการรายจ่าย</h3>
<div class="space-y-4">
<div>
<label for="edit_code" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">รหัสรายจ่าย (Code)</label>
<input type="text" id="edit_code" name="code" required class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-rose-500 bg-slate-50 dark:bg-slate-900 sm:text-sm">
</div>
<div>
<label for="edit_name" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">ชื่อรายการ (Name)</label>
<input type="text" id="edit_name" name="name" required class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-rose-500 bg-slate-50 dark:bg-slate-900 sm:text-sm">
</div>
<div>
<label for="edit_description" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">รายละเอียด (Description)</label>
<input type="text" id="edit_description" name="description" class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-rose-500 bg-slate-50 dark:bg-slate-900 sm:text-sm">
</div>
<div>
<label for="edit_display_order" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">ลำดับการแสดงผล</label>
<input type="number" id="edit_display_order" name="display_order" class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-sky-500 bg-slate-50 dark:bg-slate-900 sm:text-sm">
</div>
<div>
<label for="edit_fund_source" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">แหล่งที่มาเงิน</label>
<select id="edit_fund_source" name="fund_source" class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-sky-500 bg-slate-50 dark:bg-slate-900 sm:text-sm">
<option value="HOSPITAL">โรงพยาบาล</option>
<option value="CGD">กรมบัญชีกลาง (CGD)</option>
</select>
</div>
<div class="flex items-center justify-between mt-4">
<label class="flex items-center cursor-pointer">
<div class="relative">
<input type="checkbox" id="edit_is_tax_deductible" name="is_tax_deductible" class="sr-only">
<div class="block bg-slate-200 dark:bg-slate-700 w-10 h-6 rounded-full transition-colors"></div>
<div class="dot absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform transform translate-x-full"></div>
</div>
<div class="ml-3 text-sm font-medium text-slate-700 dark:text-slate-300">ใช้ลดหย่อนภาษีได้</div>
</label>
<label class="flex items-center cursor-pointer">
<div class="relative">
<input type="checkbox" id="edit_is_active" name="is_active" class="sr-only">
<div class="block bg-slate-200 dark:bg-slate-700 w-10 h-6 rounded-full transition-colors"></div>
<div class="dot absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform transform translate-x-full"></div>
</div>
<div class="ml-3 text-sm font-medium text-slate-700 dark:text-slate-300">เปิดใช้งาน</div>
</label>
</div>
</div>
</div>
<div class="bg-slate-50 dark:bg-slate-700/50 px-6 py-4 flex justify-end space-x-3">
<button type="button" onclick="document.getElementById('editDeductionModal').classList.add('hidden')" class="px-4 py-2 text-sm font-semibold text-slate-700 dark:text-slate-300 bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-600 rounded-xl hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors">ยกเลิก</button>
<button type="submit" class="px-4 py-2 text-sm font-semibold text-white bg-rose-500 rounded-xl hover:bg-rose-600 transition-colors">บันทึกการแก้ไข</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
/* Custom Toggle Switch Logic */
input:checked ~ .block { background-color: #0ea5e9; }
input:checked[name="is_tax_deductible"] ~ .block { background-color: #10b981; }
input:checked ~ .dot { transform: translateX(100%); }
input:not(:checked) ~ .dot { transform: translateX(0); }
</style>
<!-- jQuery and DataTables JS -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.datatables.net/2.0.8/js/dataTables.js"></script>
<script src="https://cdn.datatables.net/2.0.8/js/dataTables.tailwindcss.js"></script>
<script>
$(document).ready(function() {
$('#deductionTable').DataTable({
"language": {
"sProcessing": "กำลังดำเนินการ...",
"sLengthMenu": "แสดง _MENU_ เรคคอร์ด",
"sZeroRecords": "ไม่พบข้อมูล",
"sInfo": "แสดง _START_ ถึง _END_ จาก _TOTAL_ เรคคอร์ด",
"sInfoEmpty": "แสดง 0 ถึง 0 จาก 0 เรคคอร์ด",
"sInfoFiltered": "(กรองข้อมูล _MAX_ ทุกเรคคอร์ด)",
"sSearch": "ค้นหา:",
"oPaginate": {
"sFirst": "แรกสุด",
"sPrevious": "ก่อนหน้า",
"sNext": "ถัดไป",
"sLast": "สุดท้าย"
}
},
"pageLength": 25,
"columnDefs": [
{ "orderable": false, "targets": 5 } // Disable sorting on Action column
]
});
});
function openEditDeductionModal(deduction) {
document.getElementById('edit_id').value = deduction.id;
document.getElementById('edit_code').value = deduction.code;
document.getElementById('edit_name').value = deduction.name;
document.getElementById('edit_description').value = deduction.description;
document.getElementById('edit_fund_source').value = deduction.fund_source || 'HOSPITAL';
document.getElementById('edit_display_order').value = deduction.display_order;
document.getElementById('edit_is_tax_deductible').checked = deduction.is_tax_deductible == 1;
document.getElementById('edit_is_active').checked = deduction.is_active == 1;
document.getElementById('editDeductionModal').classList.remove('hidden');
}
</script>
<?php require_once APP_ROOT . '/app/Views/layouts/footer.php'; ?>
@@ -0,0 +1,263 @@
<?php require_once APP_ROOT . '/app/Views/layouts/header.php'; ?>
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-6 gap-4">
<div>
<h2 class="text-2xl font-bold text-slate-800 dark:text-white">รายการรายรับ (Income Categories)</h2>
<p class="text-slate-500 dark:text-slate-400 text-sm mt-1">จัดการประเภทรายรับทั้งหมดในระบบ</p>
</div>
<button onclick="document.getElementById('createIncomeModal').classList.remove('hidden')" class="inline-flex items-center justify-center px-4 py-2.5 bg-sky-500 hover:bg-sky-600 text-white text-sm font-medium rounded-xl transition-colors shadow-sm w-full md:w-auto">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /></svg>
เพิ่มรายรับใหม่
</button>
</div>
<!-- DataTables CSS for Tailwind -->
<link rel="stylesheet" href="https://cdn.datatables.net/2.0.8/css/dataTables.tailwindcss.css">
<style>
/* DataTables Dark Mode Overrides */
.dark .dt-container { color: #cbd5e1; }
.dark .dt-length select, .dark .dt-search input {
background-color: #1e293b; border-color: #334155; color: #f8fafc; border-radius: 0.5rem; padding: 0.25rem 0.5rem;
}
.dark .dt-info, .dark .dt-paging-button { color: #94a3b8 !important; }
.dark .dt-paging-button.current { background: #38bdf8 !important; color: #0f172a !important; border-color: #38bdf8 !important; }
.dark .dt-paging-button:hover { background: #334155 !important; color: #f8fafc !important; }
table.dataTable.no-footer { border-bottom: 1px solid #334155 !important; }
</style>
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-100 dark:border-slate-700 overflow-hidden p-6">
<div class="overflow-x-hidden">
<table id="incomeTable" class="w-full text-left text-sm text-slate-600 dark:text-slate-300 display">
<thead class="bg-slate-50 dark:bg-slate-900/50 text-slate-500 dark:text-slate-400 text-xs uppercase font-semibold">
<tr>
<th class="px-6 py-4">รหัส</th>
<th class="px-6 py-4">ชื่อรายการรายรับ</th>
<th class="px-6 py-4 text-center">แหล่งที่มาเงิน</th>
<th class="px-6 py-4 text-center">คิดภาษี</th>
<th class="px-6 py-4 text-center">สถานะ</th>
<th class="px-6 py-4 text-right">จัดการ</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 dark:divide-slate-700">
<?php if (empty($incomes)): ?>
<tr>
<td colspan="6" class="px-6 py-8 text-center text-slate-500 dark:text-slate-400">
ยังไม่มีข้อมูลรายการรายรับ
</td>
</tr>
<?php else: ?>
<?php foreach ($incomes as $inc): ?>
<tr class="hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors">
<td class="px-6 py-4 font-mono font-bold text-slate-700 dark:text-slate-200">
<?= htmlspecialchars($inc['code']) ?>
</td>
<td class="px-6 py-4">
<div class="font-medium text-slate-800 dark:text-slate-200"><?= htmlspecialchars($inc['name']) ?></div>
<?php if (!empty($inc['description'])): ?>
<div class="text-xs text-slate-500 dark:text-slate-400 mt-1"><?= htmlspecialchars($inc['description']) ?></div>
<?php endif; ?>
</td>
<td class="px-6 py-4 text-center font-medium text-slate-700 dark:text-slate-200">
<?= $inc['fund_source'] === 'CGD' ? 'กรมบัญชีกลาง' : 'โรงพยาบาล' ?>
</td>
<td class="px-6 py-4 text-center">
<?php if ($inc['is_taxable']): ?>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400">ใช่</span>
<?php else: ?>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-slate-100 text-slate-700 dark:bg-slate-700 dark:text-slate-300">ไม่</span>
<?php endif; ?>
</td>
<td class="px-6 py-4 text-center">
<?php if ($inc['is_active']): ?>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400">ใช้งาน</span>
<?php else: ?>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400">ปิดใช้งาน</span>
<?php endif; ?>
</td>
<td class="px-6 py-4 text-right">
<button onclick='openEditIncomeModal(<?= json_encode($inc) ?>)' class="inline-flex items-center px-3 py-1.5 border border-slate-300 dark:border-slate-600 rounded-lg text-xs font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors">
แก้ไข
</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<!-- Create Modal -->
<div id="createIncomeModal" class="hidden fixed inset-0 z-50 overflow-y-auto">
<div class="fixed inset-0 bg-slate-900/50 backdrop-blur-sm transition-opacity" onclick="document.getElementById('createIncomeModal').classList.add('hidden')"></div>
<div class="flex min-h-screen items-center justify-center p-4">
<div class="relative transform overflow-hidden rounded-2xl bg-white dark:bg-slate-800 text-left shadow-xl transition-all sm:w-full sm:max-w-lg border border-slate-100 dark:border-slate-700">
<form action="<?= BASE_URL ?>/settings/income" method="POST">
<div class="p-6">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-6">เพิ่มรายการรายรับใหม่</h3>
<div class="space-y-4">
<div>
<label for="code" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">รหัสรายรับ (Code)</label>
<input type="text" id="code" name="code" required class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-sky-500 bg-slate-50 dark:bg-slate-900 sm:text-sm">
</div>
<div>
<label for="name" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">ชื่อรายการ (Name)</label>
<input type="text" id="name" name="name" required class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-sky-500 bg-slate-50 dark:bg-slate-900 sm:text-sm">
</div>
<div>
<label for="description" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">รายละเอียด (Description)</label>
<input type="text" id="description" name="description" class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-sky-500 bg-slate-50 dark:bg-slate-900 sm:text-sm">
</div>
<div>
<label for="fund_source" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">แหล่งที่มาเงิน</label>
<select id="fund_source" name="fund_source" class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-sky-500 bg-slate-50 dark:bg-slate-900 sm:text-sm">
<option value="HOSPITAL">โรงพยาบาล</option>
<option value="CGD">กรมบัญชีกลาง (CGD)</option>
</select>
</div>
<div class="flex items-center justify-between mt-4">
<label class="flex items-center cursor-pointer">
<div class="relative">
<input type="checkbox" name="is_taxable" class="sr-only" checked>
<div class="block bg-slate-200 dark:bg-slate-700 w-10 h-6 rounded-full transition-colors"></div>
<div class="dot absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform transform translate-x-full"></div>
</div>
<div class="ml-3 text-sm font-medium text-slate-700 dark:text-slate-300">นำไปคำนวณภาษี</div>
</label>
<label class="flex items-center cursor-pointer">
<div class="relative">
<input type="checkbox" name="is_active" class="sr-only" checked>
<div class="block bg-slate-200 dark:bg-slate-700 w-10 h-6 rounded-full transition-colors"></div>
<div class="dot absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform transform translate-x-full"></div>
</div>
<div class="ml-3 text-sm font-medium text-slate-700 dark:text-slate-300">เปิดใช้งาน</div>
</label>
</div>
</div>
</div>
<div class="bg-slate-50 dark:bg-slate-700/50 px-6 py-4 flex justify-end space-x-3">
<button type="button" onclick="document.getElementById('createIncomeModal').classList.add('hidden')" class="px-4 py-2 text-sm font-semibold text-slate-700 dark:text-slate-300 bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-600 rounded-xl hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors">ยกเลิก</button>
<button type="submit" class="px-4 py-2 text-sm font-semibold text-white bg-sky-500 rounded-xl hover:bg-sky-600 transition-colors">บันทึก</button>
</div>
</form>
</div>
</div>
</div>
<!-- Edit Modal -->
<div id="editIncomeModal" class="hidden fixed inset-0 z-50 overflow-y-auto">
<div class="fixed inset-0 bg-slate-900/50 backdrop-blur-sm transition-opacity" onclick="document.getElementById('editIncomeModal').classList.add('hidden')"></div>
<div class="flex min-h-screen items-center justify-center p-4">
<div class="relative transform overflow-hidden rounded-2xl bg-white dark:bg-slate-800 text-left shadow-xl transition-all sm:w-full sm:max-w-lg border border-slate-100 dark:border-slate-700">
<form action="<?= BASE_URL ?>/settings/income/update" method="POST">
<input type="hidden" id="edit_id" name="id">
<div class="p-6">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-6">แก้ไขรายการรายรับ</h3>
<div class="space-y-4">
<div>
<label for="edit_code" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">รหัสรายรับ (Code)</label>
<input type="text" id="edit_code" name="code" required class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-sky-500 bg-slate-50 dark:bg-slate-900 sm:text-sm">
</div>
<div>
<label for="edit_name" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">ชื่อรายการ (Name)</label>
<input type="text" id="edit_name" name="name" required class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-sky-500 bg-slate-50 dark:bg-slate-900 sm:text-sm">
</div>
<div>
<label for="edit_description" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">รายละเอียด (Description)</label>
<input type="text" id="edit_description" name="description" class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-sky-500 bg-slate-50 dark:bg-slate-900 sm:text-sm">
</div>
<div>
<label for="edit_display_order" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">ลำดับการแสดงผล</label>
<input type="number" id="edit_display_order" name="display_order" class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-sky-500 bg-slate-50 dark:bg-slate-900 sm:text-sm">
</div>
<div>
<label for="edit_fund_source" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">แหล่งที่มาเงิน</label>
<select id="edit_fund_source" name="fund_source" class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-sky-500 bg-slate-50 dark:bg-slate-900 sm:text-sm">
<option value="HOSPITAL">โรงพยาบาล</option>
<option value="CGD">กรมบัญชีกลาง (CGD)</option>
</select>
</div>
<div class="flex items-center justify-between mt-4">
<label class="flex items-center cursor-pointer">
<div class="relative">
<input type="checkbox" id="edit_is_taxable" name="is_taxable" class="sr-only">
<div class="block bg-slate-200 dark:bg-slate-700 w-10 h-6 rounded-full transition-colors"></div>
<div class="dot absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform transform translate-x-full"></div>
</div>
<div class="ml-3 text-sm font-medium text-slate-700 dark:text-slate-300">นำไปคำนวณภาษี</div>
</label>
<label class="flex items-center cursor-pointer">
<div class="relative">
<input type="checkbox" id="edit_is_active" name="is_active" class="sr-only">
<div class="block bg-slate-200 dark:bg-slate-700 w-10 h-6 rounded-full transition-colors"></div>
<div class="dot absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform transform translate-x-full"></div>
</div>
<div class="ml-3 text-sm font-medium text-slate-700 dark:text-slate-300">เปิดใช้งาน</div>
</label>
</div>
</div>
</div>
<div class="bg-slate-50 dark:bg-slate-700/50 px-6 py-4 flex justify-end space-x-3">
<button type="button" onclick="document.getElementById('editIncomeModal').classList.add('hidden')" class="px-4 py-2 text-sm font-semibold text-slate-700 dark:text-slate-300 bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-600 rounded-xl hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors">ยกเลิก</button>
<button type="submit" class="px-4 py-2 text-sm font-semibold text-white bg-sky-500 rounded-xl hover:bg-sky-600 transition-colors">บันทึกการแก้ไข</button>
</div>
</form>
</div>
</div>
</div>
<style>
/* Custom Toggle Switch Logic */
input:checked ~ .block { background-color: #0ea5e9; }
input:checked ~ .dot { transform: translateX(100%); }
input:not(:checked) ~ .dot { transform: translateX(0); }
</style>
<!-- jQuery and DataTables JS -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.datatables.net/2.0.8/js/dataTables.js"></script>
<script src="https://cdn.datatables.net/2.0.8/js/dataTables.tailwindcss.js"></script>
<script>
$(document).ready(function() {
$('#incomeTable').DataTable({
"language": {
"sProcessing": "กำลังดำเนินการ...",
"sLengthMenu": "แสดง _MENU_ เรคคอร์ด",
"sZeroRecords": "ไม่พบข้อมูล",
"sInfo": "แสดง _START_ ถึง _END_ จาก _TOTAL_ เรคคอร์ด",
"sInfoEmpty": "แสดง 0 ถึง 0 จาก 0 เรคคอร์ด",
"sInfoFiltered": "(กรองข้อมูล _MAX_ ทุกเรคคอร์ด)",
"sSearch": "ค้นหา:",
"oPaginate": {
"sFirst": "แรกสุด",
"sPrevious": "ก่อนหน้า",
"sNext": "ถัดไป",
"sLast": "สุดท้าย"
}
},
"pageLength": 25,
"columnDefs": [
{ "orderable": false, "targets": 5 } // Disable sorting on Action column
]
});
});
function openEditIncomeModal(income) {
document.getElementById('edit_id').value = income.id;
document.getElementById('edit_code').value = income.code;
document.getElementById('edit_name').value = income.name;
document.getElementById('edit_description').value = income.description;
document.getElementById('edit_fund_source').value = income.fund_source || 'HOSPITAL';
document.getElementById('edit_display_order').value = income.display_order;
document.getElementById('edit_is_taxable').checked = income.is_taxable == 1;
document.getElementById('edit_is_active').checked = income.is_active == 1;
document.getElementById('editIncomeModal').classList.remove('hidden');
}
</script>
<?php require_once APP_ROOT . '/app/Views/layouts/footer.php'; ?>
@@ -0,0 +1,223 @@
<?php require_once APP_ROOT . '/app/Views/layouts/header.php'; ?>
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-6 gap-4">
<div>
<h2 class="text-2xl font-bold text-slate-800 dark:text-white">ประเภทเจ้าหน้าที่ (Officer Types)</h2>
<p class="text-slate-500 dark:text-slate-400 text-sm mt-1">จัดการประเภทเจ้าหน้าที่สำหรับใช้ในระบบรายงาน</p>
</div>
<button onclick="document.getElementById('createModal').classList.remove('hidden')" class="inline-flex items-center justify-center px-4 py-2.5 bg-sky-500 hover:bg-sky-600 text-white text-sm font-medium rounded-xl transition-colors shadow-sm w-full md:w-auto">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /></svg>
เพิ่มประเภทเจ้าหน้าที่
</button>
</div>
<!-- DataTables CSS for Tailwind -->
<link rel="stylesheet" href="https://cdn.datatables.net/2.0.8/css/dataTables.tailwindcss.css">
<style>
/* DataTables Dark Mode Overrides */
.dark .dt-container { color: #cbd5e1; }
.dark .dt-length select, .dark .dt-search input {
background-color: #1e293b; border-color: #334155; color: #f8fafc; border-radius: 0.5rem; padding: 0.25rem 0.5rem;
}
.dark .dt-info, .dark .dt-paging-button { color: #94a3b8 !important; }
.dark .dt-paging-button.current { background: #38bdf8 !important; color: #0f172a !important; border-color: #38bdf8 !important; }
.dark .dt-paging-button:hover { background: #334155 !important; color: #f8fafc !important; }
table.dataTable.no-footer { border-bottom: 1px solid #334155 !important; }
</style>
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-100 dark:border-slate-700 overflow-hidden p-6">
<div class="overflow-x-hidden">
<table id="mainTable" class="w-full text-left text-sm text-slate-600 dark:text-slate-300 display">
<thead class="bg-slate-50 dark:bg-slate-900/50 text-slate-500 dark:text-slate-400 text-xs uppercase font-semibold">
<tr>
<th class="px-6 py-4">ID</th>
<th class="px-6 py-4">ชื่อประเภทเจ้าหน้าที่</th>
<th class="px-6 py-4 text-center">สถานะ</th>
<th class="px-6 py-4 text-right">จัดการ</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 dark:divide-slate-700">
<?php if (empty($officerTypes)): ?>
<tr>
<td colspan="4" class="px-6 py-8 text-center text-slate-500 dark:text-slate-400">
ยังไม่มีข้อมูล
</td>
</tr>
<?php else: ?>
<?php foreach ($officerTypes as $type): ?>
<tr class="hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors">
<td class="px-6 py-4 font-mono text-slate-500 dark:text-slate-400">
<?= $type['id'] ?>
</td>
<td class="px-6 py-4">
<div class="font-medium text-slate-800 dark:text-slate-200"><?= htmlspecialchars($type['name']) ?></div>
</td>
<td class="px-6 py-4 text-center">
<form action="<?= BASE_URL ?>/settings/officer-types/toggle" method="POST" class="inline">
<input type="hidden" name="id" value="<?= $type['id'] ?>">
<input type="hidden" name="status" value="<?= $type['is_active'] ? '0' : '1' ?>">
<button type="submit" class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium <?= $type['is_active'] ? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400' : 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400' ?>">
<?= $type['is_active'] ? 'ใช้งาน' : 'ปิดใช้งาน' ?>
</button>
</form>
</td>
<td class="px-6 py-4 text-right space-x-2">
<button onclick='openEditModal(<?= json_encode($type) ?>)' class="inline-flex items-center px-3 py-1.5 border border-slate-300 dark:border-slate-600 rounded-lg text-xs font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors">
แก้ไข
</button>
<form action="<?= BASE_URL ?>/settings/officer-types/delete" method="POST" class="inline" onsubmit="return confirm('ยืนยันการลบประเภทเจ้าหน้าที่นี้?');">
<input type="hidden" name="id" value="<?= $type['id'] ?>">
<button type="submit" class="inline-flex items-center px-3 py-1.5 border border-red-300 dark:border-red-600/50 rounded-lg text-xs font-medium text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors">
ลบ
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<!-- Create Modal -->
<div id="createModal" class="hidden fixed inset-0 z-50 overflow-y-auto">
<div class="fixed inset-0 bg-slate-900/50 backdrop-blur-sm transition-opacity" onclick="document.getElementById('createModal').classList.add('hidden')"></div>
<div class="flex min-h-screen items-center justify-center p-4">
<div class="relative transform overflow-hidden rounded-2xl bg-white dark:bg-slate-800 text-left shadow-xl transition-all sm:w-full sm:max-w-lg border border-slate-100 dark:border-slate-700">
<form action="<?= BASE_URL ?>/settings/officer-types" method="POST">
<div class="p-6">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-6">เพิ่มประเภทเจ้าหน้าที่ใหม่</h3>
<div class="space-y-4">
<div>
<label for="name" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">ชื่อประเภทเจ้าหน้าที่</label>
<input type="text" id="name" name="name" required class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-sky-500 bg-slate-50 dark:bg-slate-900 sm:text-sm">
</div>
<div class="flex items-center justify-between mt-4">
<label class="flex items-center cursor-pointer">
<div class="relative">
<input type="checkbox" name="is_active" class="sr-only" checked>
<div class="block bg-slate-200 dark:bg-slate-700 w-10 h-6 rounded-full transition-colors"></div>
<div class="dot absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform transform translate-x-full"></div>
</div>
<div class="ml-3 text-sm font-medium text-slate-700 dark:text-slate-300">เปิดใช้งาน</div>
</label>
</div>
</div>
</div>
<div class="bg-slate-50 dark:bg-slate-700/50 px-6 py-4 flex justify-end space-x-3">
<button type="button" onclick="document.getElementById('createModal').classList.add('hidden')" class="px-4 py-2 text-sm font-semibold text-slate-700 dark:text-slate-300 bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-600 rounded-xl hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors">ยกเลิก</button>
<button type="submit" class="px-4 py-2 text-sm font-semibold text-white bg-sky-500 rounded-xl hover:bg-sky-600 transition-colors">บันทึก</button>
</div>
</form>
</div>
</div>
</div>
<!-- Edit Modal -->
<div id="editModal" class="hidden fixed inset-0 z-50 overflow-y-auto">
<div class="fixed inset-0 bg-slate-900/50 backdrop-blur-sm transition-opacity" onclick="document.getElementById('editModal').classList.add('hidden')"></div>
<div class="flex min-h-screen items-center justify-center p-4">
<div class="relative transform overflow-hidden rounded-2xl bg-white dark:bg-slate-800 text-left shadow-xl transition-all sm:w-full sm:max-w-lg border border-slate-100 dark:border-slate-700">
<form action="<?= BASE_URL ?>/settings/officer-types/update" method="POST">
<input type="hidden" name="id" id="edit_id">
<div class="p-6">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-6">แก้ไขประเภทเจ้าหน้าที่</h3>
<div class="space-y-4">
<div>
<label for="edit_name" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">ชื่อประเภทเจ้าหน้าที่</label>
<input type="text" id="edit_name" name="name" required class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-sky-500 bg-slate-50 dark:bg-slate-900 sm:text-sm">
</div>
<div class="flex items-center justify-between mt-4">
<label class="flex items-center cursor-pointer">
<div class="relative">
<input type="checkbox" id="edit_is_active" name="is_active" class="sr-only">
<div class="block bg-slate-200 dark:bg-slate-700 w-10 h-6 rounded-full transition-colors"></div>
<div class="dot absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform transform translate-x-full"></div>
</div>
<div class="ml-3 text-sm font-medium text-slate-700 dark:text-slate-300">เปิดใช้งาน</div>
</label>
</div>
</div>
</div>
<div class="bg-slate-50 dark:bg-slate-700/50 px-6 py-4 flex justify-end space-x-3">
<button type="button" onclick="document.getElementById('editModal').classList.add('hidden')" class="px-4 py-2 text-sm font-semibold text-slate-700 dark:text-slate-300 bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-600 rounded-xl hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors">ยกเลิก</button>
<button type="submit" class="px-4 py-2 text-sm font-semibold text-white bg-sky-500 rounded-xl hover:bg-sky-600 transition-colors">บันทึกการแก้ไข</button>
</div>
</form>
</div>
</div>
</div>
<!-- DataTables JS -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.datatables.net/2.0.8/js/dataTables.js"></script>
<script src="https://cdn.datatables.net/2.0.8/js/dataTables.tailwindcss.js"></script>
<script>
$(document).ready(function() {
$('#mainTable').DataTable({
"language": {
"lengthMenu": "แสดง _MENU_ รายการต่อหน้า",
"zeroRecords": "ไม่พบข้อมูล",
"info": "แสดงหน้าที่ _PAGE_ จาก _PAGES_",
"infoEmpty": "ไม่มีข้อมูล",
"infoFiltered": "(กรองจากทั้งหมด _MAX_ รายการ)",
"search": "ค้นหา:",
"paginate": {
"first": "หน้าแรก",
"last": "หน้าสุดท้าย",
"next": "ถัดไป",
"previous": "ก่อนหน้า"
}
},
"pageLength": 25,
"columnDefs": [
{ "orderable": false, "targets": [3] }
]
});
});
function openEditModal(data) {
document.getElementById('edit_id').value = data.id;
document.getElementById('edit_name').value = data.name;
const isActiveCheckbox = document.getElementById('edit_is_active');
isActiveCheckbox.checked = data.is_active == 1;
// Update toggle UI
const dot = isActiveCheckbox.nextElementSibling.nextElementSibling;
if (data.is_active == 1) {
dot.classList.add('translate-x-full');
dot.previousElementSibling.classList.replace('bg-slate-200', 'bg-sky-500');
dot.previousElementSibling.classList.replace('dark:bg-slate-700', 'dark:bg-sky-500');
} else {
dot.classList.remove('translate-x-full');
dot.previousElementSibling.classList.replace('bg-sky-500', 'bg-slate-200');
dot.previousElementSibling.classList.replace('dark:bg-sky-500', 'dark:bg-slate-700');
}
document.getElementById('editModal').classList.remove('hidden');
}
// Toggle switch logic for both modals
document.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
checkbox.addEventListener('change', function() {
const dot = this.nextElementSibling.nextElementSibling;
const bg = this.nextElementSibling;
if (this.checked) {
dot.classList.add('translate-x-full');
bg.classList.replace('bg-slate-200', 'bg-sky-500');
bg.classList.replace('dark:bg-slate-700', 'dark:bg-sky-500');
} else {
dot.classList.remove('translate-x-full');
bg.classList.replace('bg-sky-500', 'bg-slate-200');
bg.classList.replace('dark:bg-sky-500', 'dark:bg-slate-700');
}
});
});
</script>
<?php require_once APP_ROOT . '/app/Views/layouts/footer.php'; ?>
@@ -0,0 +1,423 @@
<?php require_once APP_ROOT . '/app/Views/layouts/header.php'; ?>
<div class="p-6">
<div class="mb-6 flex justify-between items-center">
<div>
<h1 class="text-2xl font-bold text-slate-800 dark:text-white">ตั้งค่าระบบทั่วไป</h1>
<p class="text-slate-500 dark:text-slate-400 text-sm mt-1">ตั้งค่ารูปแบบเอกสาร ลายเซ็น และข้อมูลอื่น ๆ ของระบบ</p>
</div>
</div>
<?php if (isset($_SESSION['success'])): ?>
<div class="mb-4 p-4 bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800 text-emerald-600 dark:text-emerald-400 rounded-xl flex items-center shadow-sm">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>
<?= htmlspecialchars($_SESSION['success']) ?>
</div>
<?php unset($_SESSION['success']); ?>
<?php endif; ?>
<?php if (isset($_SESSION['error'])): ?>
<div class="mb-4 p-4 bg-rose-50 dark:bg-rose-900/30 border border-rose-200 dark:border-rose-800 text-rose-600 dark:text-rose-400 rounded-xl flex items-center shadow-sm">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<?= htmlspecialchars($_SESSION['error']) ?>
</div>
<?php unset($_SESSION['error']); ?>
<?php endif; ?>
<!-- Tabs Navigation -->
<div class="bg-white dark:bg-slate-800 rounded-t-2xl px-6 pt-4 border-b border-slate-200 dark:border-slate-700 shadow-sm mt-4">
<nav class="-mb-px flex space-x-8" aria-label="Tabs">
<button onclick="switchTab('tab-general')" id="btn-general" class="tab-btn border-sky-500 text-sky-600 dark:text-sky-400 whitespace-nowrap border-b-2 py-4 px-1 text-base font-semibold transition-colors flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 21h16.5M4.5 3h15M5.25 3v18m13.5-18v18M9 6.75h1.5m-1.5 3h1.5m-1.5 3h1.5m3-6H15m-1.5 3H15m-1.5 3H15M9 21v-3.375c0-.621.504-1.125 1.125-1.125h3.75c.621 0 1.125.504 1.125 1.125V21" /></svg>
ข้อมูลทั่วไป
</button>
<button onclick="switchTab('tab-slip')" id="btn-slip" class="tab-btn border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-300 whitespace-nowrap border-b-2 py-4 px-1 text-base font-semibold transition-colors flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 18.75a60.07 60.07 0 0115.797 2.101c.727.198 1.453-.342 1.453-1.096V18.75M3.75 4.5v.75A.75.75 0 013 6h-.75m0 0v-.375c0-.621.504-1.125 1.125-1.125H20.25M2.25 6v9m18-10.5v.75c0 .414.336.75.75.75h.75m-1.5-1.5h.375c.621 0 1.125.504 1.125 1.125v9.75c0 .621-.504 1.125-1.125 1.125h-.375m1.5-1.5H21a.75.75 0 00-.75.75v.75m0 0H3.75m0 0h-.375a1.125 1.125 0 01-1.125-1.125V15m1.5 1.5v-.75A.75.75 0 003 15h-.75M15 10.5a3 3 0 11-6 0 3 3 0 016 0zm3 0h.008v.008H18V10.5zm-12 0h.008v.008H6V10.5z" /></svg>
สลิปเงินเดือน
</button>
<button onclick="switchTab('tab-tax')" id="btn-tax" class="tab-btn border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-300 whitespace-nowrap border-b-2 py-4 px-1 text-base font-semibold transition-colors flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m3.75 9v6m3-3H9m1.5-12H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" /></svg>
ใบเสียภาษี (50 ทวิ)
</button>
</nav>
</div>
<!-- MAIN FORM FOR SAVING TEXT SETTINGS -->
<form action="<?= BASE_URL ?>/settings/system/save" method="POST" id="mainSettingsForm"></form>
<!-- Tab 1: General -->
<div id="tab-general" class="tab-content bg-white dark:bg-slate-800 rounded-b-2xl shadow-sm p-6 mb-8 border border-slate-100 dark:border-slate-700 border-t-0">
<div class="max-w-3xl">
<h3 class="text-lg font-bold text-slate-800 dark:text-white mb-6">ข้อมูลทั่วไปของหน่วยงานและระบบ</h3>
<div class="mb-6">
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">เลขประจำตัวผู้เสียภาษีของหน่วยงาน</label>
<input type="text" name="org_tax_id" form="mainSettingsForm" value="<?= htmlspecialchars($orgTaxId ?? '') ?>" placeholder="เช่น 0994000164801"
class="w-full px-4 py-3 border border-slate-200 dark:border-slate-700 rounded-xl bg-slate-50 dark:bg-slate-900 text-slate-800 dark:text-slate-200 focus:outline-none focus:ring-2 focus:ring-sky-500 transition-all">
<p class="text-xs text-slate-500 mt-2">ข้อมูลนี้จะถูกนำไปแสดงในหนังสือรับรองการหักภาษี ณ ที่จ่าย (50 ทวิ)</p>
</div>
<div class="mb-6">
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">เวอร์ชันของระบบ (App Version)</label>
<input type="text" name="app_version" form="mainSettingsForm" value="<?= htmlspecialchars($appVersion ?? '1.0.0') ?>" placeholder="เช่น 1.0.0"
class="w-full px-4 py-3 border border-slate-200 dark:border-slate-700 rounded-xl bg-slate-50 dark:bg-slate-900 text-slate-800 dark:text-slate-200 focus:outline-none focus:ring-2 focus:ring-sky-500 transition-all">
<p class="text-xs text-slate-500 mt-2">เลขเวอร์ชันที่จะแสดงในหน้าต่างๆ ของระบบ เช่น หน้าล็อกอิน หรือส่วนท้ายเว็บ</p>
</div>
<div class="mb-8">
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">แสดงปีย้อนหลังในกราฟ Dashboard (จำนวนปี)</label>
<input type="number" name="chart_history_years" form="mainSettingsForm" value="<?= htmlspecialchars($chartHistoryYears ?? '3') ?>" min="1" max="10"
class="w-full px-4 py-3 border border-slate-200 dark:border-slate-700 rounded-xl bg-slate-50 dark:bg-slate-900 text-slate-800 dark:text-slate-200 focus:outline-none focus:ring-2 focus:ring-sky-500 transition-all">
</div>
<hr class="border-slate-100 dark:border-slate-700 mb-6">
<button type="submit" form="mainSettingsForm" class="px-6 py-3 bg-sky-600 hover:bg-sky-700 text-white rounded-xl text-sm font-semibold transition-colors shadow flex items-center justify-center w-full md:w-auto">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" /></svg>
บันทึกการตั้งค่าข้อมูลทั่วไป
</button>
</div>
</div>
<!-- Tab 2: Salary Slip -->
<div id="tab-slip" class="tab-content hidden bg-white dark:bg-slate-800 rounded-b-2xl shadow-sm p-6 mb-8 border border-slate-100 dark:border-slate-700 border-t-0">
<h3 class="text-lg font-bold text-slate-800 dark:text-white mb-6">ตั้งค่าสลิปเงินเดือน</h3>
<div class="mb-8 max-w-xl">
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">ขนาดตัวอักษรสลิปเงินเดือน</label>
<select name="slip_font_size" form="mainSettingsForm" class="w-full px-4 py-3 border border-slate-200 dark:border-slate-700 rounded-xl bg-slate-50 dark:bg-slate-900 text-slate-800 dark:text-slate-200 focus:outline-none focus:ring-2 focus:ring-sky-500 transition-all">
<option value="10pt" <?= ($slipFontSize ?? '11pt') === '10pt' ? 'selected' : '' ?>>10pt (เล็ก)</option>
<option value="11pt" <?= ($slipFontSize ?? '11pt') === '11pt' ? 'selected' : '' ?>>11pt (มาตรฐาน)</option>
<option value="12pt" <?= ($slipFontSize ?? '11pt') === '12pt' ? 'selected' : '' ?>>12pt (ใหญ่)</option>
<option value="14pt" <?= ($slipFontSize ?? '11pt') === '14pt' ? 'selected' : '' ?>>14pt (ใหญ่มาก)</option>
</select>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Details & Positioning -->
<div>
<h4 class="text-md font-semibold text-slate-700 dark:text-slate-300 mb-4 border-b border-slate-100 dark:border-slate-700 pb-2">รายละเอียดข้อความใต้ลายเซ็น</h4>
<div class="mb-4 relative">
<label class="block text-xs text-slate-500 mb-1">ค้นหาเจ้าหน้าที่จากระบบ HR (ตัวช่วยเติมคำ)</label>
<input type="text" id="officerSearch" placeholder="พิมพ์ชื่อ, นามสกุล หรือเลขบัตร ปชช. เพื่อค้นหา..."
class="w-full px-4 py-2 border border-slate-200 dark:border-slate-700 rounded-lg bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-200 focus:outline-none focus:ring-2 focus:ring-sky-500 transition-all">
<ul id="searchResults" class="absolute z-10 w-full bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg mt-1 max-h-60 overflow-y-auto hidden shadow-lg"></ul>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">ชื่อ-สกุล</label>
<input type="text" id="signature_name" name="signature_name" form="mainSettingsForm" value="<?= htmlspecialchars($signatureName ?? '') ?>" placeholder="เช่น ( นางณัฐฐิณี เรืองทอง )"
class="w-full px-4 py-3 border border-slate-200 dark:border-slate-700 rounded-xl bg-slate-50 dark:bg-slate-900 text-slate-800 dark:text-slate-200 focus:outline-none focus:ring-2 focus:ring-sky-500 transition-all">
</div>
<div class="mb-8">
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">ตำแหน่ง</label>
<input type="text" id="signature_position" name="signature_position" form="mainSettingsForm" value="<?= htmlspecialchars($signaturePosition ?? '') ?>" placeholder="เช่น นักวิชาการเงินและบัญชี"
class="w-full px-4 py-3 border border-slate-200 dark:border-slate-700 rounded-xl bg-slate-50 dark:bg-slate-900 text-slate-800 dark:text-slate-200 focus:outline-none focus:ring-2 focus:ring-sky-500 transition-all">
</div>
<h4 class="text-md font-semibold text-slate-700 dark:text-slate-300 mb-4 border-b border-slate-100 dark:border-slate-700 pb-2">การจัดตำแหน่งรูปลายเซ็น</h4>
<div class="mb-6">
<label class="flex justify-between items-center text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
<span>ขนาดรูปลายเซ็น (Scale)</span>
<span id="scaleValue" class="text-sky-600 font-bold bg-sky-50 dark:bg-sky-900/40 px-2 py-1 rounded"><?= htmlspecialchars($signatureScale ?? '100') ?>%</span>
</label>
<input type="range" name="signature_scale" form="mainSettingsForm" min="50" max="200" step="5" value="<?= htmlspecialchars($signatureScale ?? '100') ?>"
oninput="document.getElementById('scaleValue').innerText = this.value + '%'"
class="w-full h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer dark:bg-slate-700 accent-sky-500">
</div>
<div class="grid grid-cols-2 gap-4 mb-6">
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">ขยับซ้าย-ขวา (px)</label>
<input type="number" name="signature_pos_x" form="mainSettingsForm" value="<?= htmlspecialchars($signaturePosX ?? '0') ?>"
class="w-full px-4 py-3 border border-slate-200 dark:border-slate-700 rounded-xl bg-slate-50 dark:bg-slate-900 text-slate-800 dark:text-slate-200 focus:outline-none focus:ring-2 focus:ring-sky-500 transition-all">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">ขยับขึ้น-ลง (px)</label>
<input type="number" name="signature_pos_y" form="mainSettingsForm" value="<?= htmlspecialchars($signaturePosY ?? '0') ?>"
class="w-full px-4 py-3 border border-slate-200 dark:border-slate-700 rounded-xl bg-slate-50 dark:bg-slate-900 text-slate-800 dark:text-slate-200 focus:outline-none focus:ring-2 focus:ring-sky-500 transition-all">
</div>
</div>
<button type="submit" form="mainSettingsForm" class="w-full px-6 py-3 bg-sky-600 hover:bg-sky-700 text-white rounded-xl text-sm font-semibold transition-colors shadow flex items-center justify-center mt-6">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" /></svg>
บันทึกการตั้งค่าสลิปเงินเดือน
</button>
</div>
<!-- Upload Signature (Separate Form) -->
<div>
<h4 class="text-md font-semibold text-slate-700 dark:text-slate-300 mb-4 border-b border-slate-100 dark:border-slate-700 pb-2">อัปโหลดลายเซ็นใหม่</h4>
<div class="mb-6">
<p class="text-sm text-slate-600 dark:text-slate-400 mb-2">รูปลายเซ็นปัจจุบัน</p>
<div class="border-2 border-dashed border-slate-300 dark:border-slate-600 rounded-xl p-4 flex justify-center items-center bg-slate-50 dark:bg-slate-900/50 min-h-[150px]">
<?php
$sigPath = APP_ROOT . '/public/img/signature.png';
if (file_exists($sigPath)):
$modTime = filemtime($sigPath);
?>
<img src="<?= BASE_URL ?>/public/img/signature.png?t=<?= $modTime ?>" alt="Current Signature" class="max-h-32 object-contain mix-blend-multiply dark:mix-blend-normal">
<?php else: ?>
<div class="text-slate-400 dark:text-slate-500 flex flex-col items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 h-8 mb-2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
<span>ยังไม่มีรูปภาพลายเซ็น</span>
</div>
<?php endif; ?>
</div>
</div>
<form action="<?= BASE_URL ?>/settings/system" method="POST" enctype="multipart/form-data" class="bg-slate-50 dark:bg-slate-800/50 p-4 rounded-xl border border-slate-200 dark:border-slate-700">
<div class="mb-4">
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">เลือกไฟล์รูปภาพ (JPG/PNG)</label>
<input type="file" name="signature" accept="image/jpeg, image/png" required
class="block w-full text-sm text-slate-500 dark:text-slate-400
file:mr-4 file:py-2 file:px-4
file:rounded-lg file:border-0
file:text-sm file:font-medium
file:bg-sky-50 file:text-sky-700
dark:file:bg-sky-900/30 dark:file:text-sky-400
hover:file:bg-sky-100 dark:hover:file:bg-sky-900/50
transition-colors cursor-pointer border border-slate-200 dark:border-slate-700 rounded-lg p-2 bg-white dark:bg-slate-900">
</div>
<button type="submit" class="w-full px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-sm font-medium transition-colors shadow-sm flex justify-center items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" /></svg>
อัปโหลดลายเซ็นสลิป
</button>
</form>
</div>
</div>
</div>
<!-- Tab 3: Tax Certificate -->
<div id="tab-tax" class="tab-content hidden bg-white dark:bg-slate-800 rounded-b-2xl shadow-sm p-6 mb-8 border border-slate-100 dark:border-slate-700 border-t-0">
<h3 class="text-lg font-bold text-slate-800 dark:text-white mb-6">ตั้งค่าหนังสือรับรองการหักภาษี ณ ที่จ่าย (50 ทวิ)</h3>
<div class="mb-8 max-w-xl">
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">ขนาดตัวอักษรใบเสียภาษี</label>
<select name="tax_font_size" form="mainSettingsForm" class="w-full px-4 py-3 border border-slate-200 dark:border-slate-700 rounded-xl bg-slate-50 dark:bg-slate-900 text-slate-800 dark:text-slate-200 focus:outline-none focus:ring-2 focus:ring-sky-500 transition-all">
<option value="10pt" <?= ($taxFontSize ?? '11pt') === '10pt' ? 'selected' : '' ?>>10pt (เล็ก)</option>
<option value="11pt" <?= ($taxFontSize ?? '11pt') === '11pt' ? 'selected' : '' ?>>11pt (มาตรฐาน)</option>
<option value="12pt" <?= ($taxFontSize ?? '11pt') === '12pt' ? 'selected' : '' ?>>12pt (ใหญ่)</option>
<option value="14pt" <?= ($taxFontSize ?? '11pt') === '14pt' ? 'selected' : '' ?>>14pt (ใหญ่มาก)</option>
</select>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">ที่อยู่ผู้มีหน้าที่หักภาษี (หน่วยงาน)</label>
<textarea name="org_address" form="mainSettingsForm" rows="2" placeholder="ต.อ่างทอง อ.เกาะสมุย จ.สุราษฎร์ธานี 84140"
class="w-full px-4 py-3 border border-slate-200 dark:border-slate-700 rounded-xl bg-slate-50 dark:bg-slate-900 text-slate-800 dark:text-slate-200 focus:outline-none focus:ring-2 focus:ring-sky-500 transition-all"><?= htmlspecialchars($orgAddress ?? '') ?></textarea>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">ที่อยู่ผู้ถูกหักภาษี (ค่าเริ่มต้น)</label>
<textarea name="tax_payee_address" form="mainSettingsForm" rows="2" placeholder="ต.อ่างทอง อ.เกาะสมุย จ.สุราษฎร์ธานี 84140"
class="w-full px-4 py-3 border border-slate-200 dark:border-slate-700 rounded-xl bg-slate-50 dark:bg-slate-900 text-slate-800 dark:text-slate-200 focus:outline-none focus:ring-2 focus:ring-sky-500 transition-all"><?= htmlspecialchars($taxPayeeAddress ?? '') ?></textarea>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Details & Positioning -->
<div>
<h4 class="text-md font-semibold text-slate-700 dark:text-slate-300 mb-4 border-b border-slate-100 dark:border-slate-700 pb-2">รายละเอียดข้อความใต้ลายเซ็น</h4>
<div class="mb-4 relative">
<label class="block text-xs text-slate-500 mb-1">ค้นหาเจ้าหน้าที่จากระบบ HR (ตัวช่วยเติมคำ)</label>
<input type="text" id="taxOfficerSearch" placeholder="พิมพ์ชื่อ, นามสกุล หรือเลขบัตร ปชช. เพื่อค้นหา..."
class="w-full px-4 py-2 border border-slate-200 dark:border-slate-700 rounded-lg bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-200 focus:outline-none focus:ring-2 focus:ring-sky-500 transition-all">
<ul id="taxSearchResults" class="absolute z-10 w-full bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg mt-1 max-h-60 overflow-y-auto hidden shadow-lg"></ul>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">ชื่อ-สกุล</label>
<input type="text" id="tax_signature_name" name="tax_signature_name" form="mainSettingsForm" value="<?= htmlspecialchars($taxSignatureName ?? '') ?>" placeholder="เช่น ( นางสาวกมลวรรณ บุญบวร )"
class="w-full px-4 py-3 border border-slate-200 dark:border-slate-700 rounded-xl bg-slate-50 dark:bg-slate-900 text-slate-800 dark:text-slate-200 focus:outline-none focus:ring-2 focus:ring-sky-500 transition-all">
</div>
<div class="mb-8">
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">ตำแหน่ง</label>
<input type="text" id="tax_signature_position" name="tax_signature_position" form="mainSettingsForm" value="<?= htmlspecialchars($taxSignaturePosition ?? '') ?>" placeholder="เช่น นักวิชาการเงินและบัญชีปฏิบัติการ"
class="w-full px-4 py-3 border border-slate-200 dark:border-slate-700 rounded-xl bg-slate-50 dark:bg-slate-900 text-slate-800 dark:text-slate-200 focus:outline-none focus:ring-2 focus:ring-sky-500 transition-all">
</div>
<h4 class="text-md font-semibold text-slate-700 dark:text-slate-300 mb-4 border-b border-slate-100 dark:border-slate-700 pb-2">การจัดตำแหน่งรูปลายเซ็น</h4>
<div class="mb-6">
<label class="flex justify-between items-center text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
<span>ขนาดรูปลายเซ็น (Scale)</span>
<span id="taxScaleValue" class="text-sky-600 font-bold bg-sky-50 dark:bg-sky-900/40 px-2 py-1 rounded"><?= htmlspecialchars($taxSignatureScale ?? '100') ?>%</span>
</label>
<input type="range" name="tax_signature_scale" form="mainSettingsForm" min="50" max="200" step="5" value="<?= htmlspecialchars($taxSignatureScale ?? '100') ?>"
oninput="document.getElementById('taxScaleValue').innerText = this.value + '%'"
class="w-full h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer dark:bg-slate-700 accent-sky-500">
</div>
<div class="grid grid-cols-2 gap-4 mb-6">
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">ขยับซ้าย-ขวา (px)</label>
<input type="number" name="tax_signature_pos_x" form="mainSettingsForm" value="<?= htmlspecialchars($taxSignaturePosX ?? '0') ?>"
class="w-full px-4 py-3 border border-slate-200 dark:border-slate-700 rounded-xl bg-slate-50 dark:bg-slate-900 text-slate-800 dark:text-slate-200 focus:outline-none focus:ring-2 focus:ring-sky-500 transition-all">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">ขยับขึ้น-ลง (px)</label>
<input type="number" name="tax_signature_pos_y" form="mainSettingsForm" value="<?= htmlspecialchars($taxSignaturePosY ?? '0') ?>"
class="w-full px-4 py-3 border border-slate-200 dark:border-slate-700 rounded-xl bg-slate-50 dark:bg-slate-900 text-slate-800 dark:text-slate-200 focus:outline-none focus:ring-2 focus:ring-sky-500 transition-all">
</div>
</div>
<button type="submit" form="mainSettingsForm" class="w-full px-6 py-3 bg-sky-600 hover:bg-sky-700 text-white rounded-xl text-sm font-semibold transition-colors shadow flex items-center justify-center mt-6">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" /></svg>
บันทึกการตั้งค่าใบเสียภาษี
</button>
</div>
<!-- Upload Signature (Separate Form) -->
<div>
<h4 class="text-md font-semibold text-slate-700 dark:text-slate-300 mb-4 border-b border-slate-100 dark:border-slate-700 pb-2">อัปโหลดลายเซ็นใหม่</h4>
<div class="mb-6">
<p class="text-sm text-slate-600 dark:text-slate-400 mb-2">รูปลายเซ็นปัจจุบัน</p>
<div class="border-2 border-dashed border-slate-300 dark:border-slate-600 rounded-xl p-4 flex justify-center items-center bg-slate-50 dark:bg-slate-900/50 min-h-[150px]">
<?php
$taxSigPath = APP_ROOT . '/public/img/tax_signature.png';
if (file_exists($taxSigPath)):
$modTime = filemtime($taxSigPath);
?>
<img src="<?= BASE_URL ?>/public/img/tax_signature.png?t=<?= $modTime ?>" alt="Tax Signature" class="max-h-32 object-contain mix-blend-multiply dark:mix-blend-normal">
<?php else: ?>
<div class="text-slate-400 dark:text-slate-500 flex flex-col items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 h-8 mb-2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
<span>ยังไม่มีรูปภาพลายเซ็นใบเสียภาษี</span>
</div>
<?php endif; ?>
</div>
</div>
<form action="<?= BASE_URL ?>/settings/system/upload-tax-signature" method="POST" enctype="multipart/form-data" class="bg-slate-50 dark:bg-slate-800/50 p-4 rounded-xl border border-slate-200 dark:border-slate-700">
<div class="mb-4">
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">เลือกไฟล์รูปภาพ (JPG/PNG)</label>
<input type="file" name="tax_signature" accept="image/jpeg, image/png" required
class="block w-full text-sm text-slate-500 dark:text-slate-400
file:mr-4 file:py-2 file:px-4
file:rounded-lg file:border-0
file:text-sm file:font-medium
file:bg-sky-50 file:text-sky-700
dark:file:bg-sky-900/30 dark:file:text-sky-400
hover:file:bg-sky-100 dark:hover:file:bg-sky-900/50
transition-colors cursor-pointer border border-slate-200 dark:border-slate-700 rounded-lg p-2 bg-white dark:bg-slate-900">
</div>
<button type="submit" class="w-full px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-sm font-medium transition-colors shadow-sm flex justify-center items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" /></svg>
อัปโหลดลายเซ็นใบเสียภาษี
</button>
</form>
</div>
</div>
</div>
</div>
<script>
// Tab Switching Logic
function switchTab(tabId) {
// Hide all tab contents
document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
// Show target tab
document.getElementById(tabId).classList.remove('hidden');
// Reset all buttons
document.querySelectorAll('.tab-btn').forEach(el => {
el.classList.remove('border-sky-500', 'text-sky-600', 'dark:text-sky-400');
el.classList.add('border-transparent', 'text-slate-500');
});
// Activate target button
const activeBtn = document.getElementById(tabId.replace('tab-', 'btn-'));
if (activeBtn) {
activeBtn.classList.remove('border-transparent', 'text-slate-500');
activeBtn.classList.add('border-sky-500', 'text-sky-600', 'dark:text-sky-400');
}
// Save state to localStorage to remember tab after form submit
localStorage.setItem('activeSettingsTab', tabId);
}
document.addEventListener('DOMContentLoaded', function() {
// Restore active tab
const savedTab = localStorage.getItem('activeSettingsTab') || 'tab-general';
switchTab(savedTab);
// HR Search logic
const searchInput = document.getElementById('officerSearch');
const searchResults = document.getElementById('searchResults');
const nameInput = document.getElementById('signature_name');
const positionInput = document.getElementById('signature_position');
const taxSearchInput = document.getElementById('taxOfficerSearch');
const taxSearchResults = document.getElementById('taxSearchResults');
const taxNameInput = document.getElementById('tax_signature_name');
const taxPositionInput = document.getElementById('tax_signature_position');
let debounceTimer;
let taxDebounceTimer;
function setupSearch(inputEl, resultsEl, nameEl, posEl, timer) {
inputEl.addEventListener('input', function() {
clearTimeout(timer);
const keyword = this.value.trim();
if (keyword.length < 2) {
resultsEl.classList.add('hidden');
return;
}
timer = setTimeout(function() {
fetch('<?= BASE_URL ?>/settings/system/search-officer?q=' + encodeURIComponent(keyword))
.then(response => {
if (!response.ok) throw new Error('Network error ' + response.status);
return response.text();
})
.then(text => {
let res;
try {
res = JSON.parse(text);
} catch (e) {
console.error('Invalid JSON from server:', text);
alert('เกิดข้อผิดพลาดในการดึงข้อมูลจากเซิร์ฟเวอร์\nกรุณาดูรายละเอียดใน Console');
return;
}
resultsEl.innerHTML = '';
if (res.status === 'success' && res.data && res.data.length > 0) {
res.data.forEach(item => {
const li = document.createElement('li');
li.className = 'px-4 py-2 hover:bg-sky-50 dark:hover:bg-slate-700 cursor-pointer text-sm text-slate-700 dark:text-slate-300 border-b border-slate-100 dark:border-slate-700 last:border-0';
li.textContent = item.text;
li.addEventListener('click', function() {
nameEl.value = '( ' + item.name + ' )';
posEl.value = item.position;
inputEl.value = '';
resultsEl.classList.add('hidden');
});
resultsEl.appendChild(li);
});
resultsEl.classList.remove('hidden');
} else {
const li = document.createElement('li');
li.className = 'px-4 py-2 text-sm text-slate-500 dark:text-slate-400 italic';
li.textContent = res.message || 'ไม่พบข้อมูล';
resultsEl.appendChild(li);
resultsEl.classList.remove('hidden');
}
})
.catch(error => {
console.error('Error searching:', error);
});
}, 300);
});
document.addEventListener('click', function(e) {
if (!inputEl.contains(e.target) && !resultsEl.contains(e.target)) {
resultsEl.classList.add('hidden');
}
});
}
if (searchInput) setupSearch(searchInput, searchResults, nameInput, positionInput, debounceTimer);
if (taxSearchInput) setupSearch(taxSearchInput, taxSearchResults, taxNameInput, taxPositionInput, taxDebounceTimer);
});
</script>
<?php require_once APP_ROOT . '/app/Views/layouts/footer.php'; ?>
@@ -0,0 +1,304 @@
<?php require_once APP_ROOT . '/app/Views/layouts/header.php'; ?>
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-6 gap-4">
<div>
<h2 class="text-2xl font-bold text-slate-800 dark:text-white">จัดการผู้ใช้งานระบบ</h2>
<p class="text-slate-500 dark:text-slate-400 text-sm mt-1">กำหนดสิทธิ์และเพิ่มผู้ใช้งานที่สามารถเข้าระบบได้</p>
</div>
<div class="flex gap-2">
<a href="<?= BASE_URL ?>/users/logs" class="inline-flex items-center justify-center px-4 py-2.5 bg-white border border-slate-300 dark:border-slate-600 dark:bg-slate-800 hover:bg-slate-50 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 text-sm font-medium rounded-xl transition-colors shadow-sm w-full md:w-auto">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" /></svg>
ประวัติการใช้งาน (Logs)
</a>
<button onclick="document.getElementById('createUserModal').classList.remove('hidden')" class="inline-flex items-center justify-center px-4 py-2.5 bg-sky-500 hover:bg-sky-600 text-white text-sm font-medium rounded-xl transition-colors shadow-sm w-full md:w-auto">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M18 18.72a9.094 9.094 0 0 0 3.741-.479 3 3 0 0 0-4.682-2.72m.94 3.198.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0 1 12 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 0 1 6 18.719m12 0a5.971 5.971 0 0 0-.941-3.197m0 0A5.995 5.995 0 0 0 12 12.75a5.995 5.995 0 0 0-5.058 2.772m0 0a3 3 0 0 0-4.681 2.72 8.986 8.986 0 0 0 3.74.477m.94-3.197a5.971 5.971 0 0 0-.94 3.197M15 6.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm6 3a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Zm-13.5 0a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Z" /></svg>
เพิ่มผู้ใช้งาน
</button>
</div>
</div>
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-100 dark:border-slate-700 overflow-hidden">
<div class="flex justify-end p-4 pb-0">
<label class="inline-flex items-center cursor-pointer">
<input type="checkbox" id="togglePrefix" class="w-4 h-4 text-indigo-600 bg-slate-100 border-slate-300 rounded focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:ring-offset-slate-800 focus:ring-2 dark:bg-slate-700 dark:border-slate-600">
<span class="ml-2 text-sm font-medium text-slate-700 dark:text-slate-300">แสดงคำนำหน้าชื่อ</span>
</label>
</div>
<div class="overflow-x-auto p-4 pt-2">
<table class="w-full text-left text-sm text-slate-600 dark:text-slate-300">
<thead class="bg-slate-50 dark:bg-slate-900/50 text-slate-500 dark:text-slate-400 text-xs uppercase font-semibold">
<tr>
<th class="px-6 py-4">Username</th>
<th class="px-6 py-4">ชื่อ - นามสกุล</th>
<th class="px-6 py-4">สิทธิ์ (Role)</th>
<th class="px-6 py-4 text-center">สถานะ</th>
<th class="px-6 py-4 text-right">จัดการ</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 dark:divide-slate-700">
<?php if (empty($users)): ?>
<tr>
<td colspan="5" class="px-6 py-8 text-center text-slate-500 dark:text-slate-400">
ไม่พบข้อมูล
</td>
</tr>
<?php else: ?>
<?php foreach ($users as $user): ?>
<tr class="hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors">
<td class="px-6 py-4 font-mono font-bold text-sky-600 dark:text-sky-400">
<?= htmlspecialchars($user['username']) ?>
</td>
<td class="px-6 py-4" data-sort="<?= htmlspecialchars(preg_replace('/^(นาย|นางสาว|นาง|ว่าที่ร้อยตรี|น\.ส\.|นพ\.|พญ\.|ดร\.)\s*/u', '', trim($user['first_name']))) ?>">
<?php
$rawName = trim($user['first_name']);
$prefix = '';
$bodyName = $rawName;
if (preg_match('/^(นาย|นางสาว|นาง|ว่าที่ร้อยตรี|น\.ส\.|นพ\.|พญ\.|ดร\.)\s*(.*)$/u', $rawName, $matches)) {
$prefix = $matches[1];
$bodyName = $matches[2];
}
?>
<div class="font-medium text-slate-800 dark:text-slate-200">
<span class="name-prefix hidden"><?= htmlspecialchars($prefix) ?></span><?= htmlspecialchars($bodyName . ' ' . $user['last_name']) ?>
</div>
</td>
<td class="px-6 py-4">
<?php if ($user['role_id'] == 1): ?>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400">
<?= htmlspecialchars($user['role_name']) ?>
</span>
<?php else: ?>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-slate-100 text-slate-700 dark:bg-slate-700 dark:text-slate-300">
<?= htmlspecialchars($user['role_name']) ?>
</span>
<?php endif; ?>
</td>
<td class="px-6 py-4 text-center">
<?php if ($user['status'] == 'active'): ?>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400">Active</span>
<?php else: ?>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400">Inactive</span>
<?php endif; ?>
</td>
<td class="px-6 py-4 text-right">
<div class="flex items-center justify-end space-x-2">
<?php if ($user['status'] == 'active'): ?>
<form action="<?= BASE_URL ?>/users/toggle_status" method="POST" class="inline">
<input type="hidden" name="user_id" value="<?= $user['id'] ?>">
<input type="hidden" name="status" value="inactive">
<button type="submit" onclick="return confirm('ยืนยันการปิดสิทธิ์ผู้ใช้งานนี้?')" class="inline-flex items-center px-3 py-1.5 border border-red-200 dark:border-red-800/30 bg-red-50 text-red-600 dark:bg-red-900/20 dark:text-red-400 rounded-lg text-xs font-medium hover:bg-red-100 dark:hover:bg-red-900/40 transition-colors" title="ปิดสิทธิ์">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-3.5 h-3.5 mr-1"><path stroke-linecap="round" stroke-linejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" /></svg>
ปิดสิทธิ์
</button>
</form>
<?php else: ?>
<form action="<?= BASE_URL ?>/users/toggle_status" method="POST" class="inline">
<input type="hidden" name="user_id" value="<?= $user['id'] ?>">
<input type="hidden" name="status" value="active">
<button type="submit" class="inline-flex items-center px-3 py-1.5 border border-emerald-200 dark:border-emerald-800/30 bg-emerald-50 text-emerald-600 dark:bg-emerald-900/20 dark:text-emerald-400 rounded-lg text-xs font-medium hover:bg-emerald-100 dark:hover:bg-emerald-900/40 transition-colors" title="เปิดสิทธิ์">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-3.5 h-3.5 mr-1"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0Z" /></svg>
เปิดสิทธิ์
</button>
</form>
<?php endif; ?>
<button onclick="openEditModal(<?= htmlspecialchars(json_encode($user)) ?>)" class="inline-flex items-center px-3 py-1.5 border border-slate-300 dark:border-slate-600 rounded-lg text-xs font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors">
แก้ไข
</button>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<!-- Create Modal -->
<div id="createUserModal" class="hidden fixed inset-0 z-50 overflow-y-auto">
<div class="fixed inset-0 bg-slate-900/50 backdrop-blur-sm transition-opacity" onclick="document.getElementById('createUserModal').classList.add('hidden')"></div>
<div class="flex min-h-screen items-center justify-center p-4">
<div class="relative transform overflow-hidden rounded-2xl bg-white dark:bg-slate-800 text-left shadow-xl transition-all sm:w-full sm:max-w-lg border border-slate-100 dark:border-slate-700">
<form action="<?= BASE_URL ?>/users" method="POST">
<div class="p-6">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-6">เพิ่มผู้ใช้งานใหม่</h3>
<div class="space-y-4">
<div>
<label for="hr_search" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">ค้นหาจากระบบบุคลากร (HR)</label>
<select id="hr_search" class="searchable-select block w-full"></select>
<p class="text-xs text-slate-500 mt-1">พิมพ์ชื่อ, นามสกุล เพื่อดึงข้อมูลอัตโนมัติ</p>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label for="first_name" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">ชื่อ (First Name)</label>
<input type="text" id="first_name" name="first_name" required class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-sky-500 bg-slate-50 dark:bg-slate-900 sm:text-sm">
</div>
<div>
<label for="last_name" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">นามสกุล (Last Name)</label>
<input type="text" id="last_name" name="last_name" required class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-sky-500 bg-slate-50 dark:bg-slate-900 sm:text-sm">
</div>
</div>
<div>
<label for="username" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">ชื่อผู้ใช้ (Username)</label>
<input type="text" id="username" name="username" required autocomplete="off" class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-sky-500 bg-slate-50 dark:bg-slate-900 sm:text-sm">
<p class="text-xs text-slate-500 mt-1">แนะนำให้ใช้รหัสบัตรประชาชน 13 หลัก</p>
</div>
<div>
<label for="password" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">รหัสผ่าน (Password)</label>
<input type="password" id="password" name="password" required class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-sky-500 bg-slate-50 dark:bg-slate-900 sm:text-sm">
</div>
<div>
<label for="role_id" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">สิทธิ์การใช้งาน (Role)</label>
<select id="role_id" name="role_id" required class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-sky-500 bg-slate-50 dark:bg-slate-900 sm:text-sm">
<?php foreach($roles as $role): ?>
<option value="<?= $role['id'] ?>"><?= htmlspecialchars($role['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="flex items-center justify-between mt-4">
<label class="flex items-center cursor-pointer">
<div class="relative">
<input type="checkbox" name="status" class="sr-only" checked>
<div class="block bg-slate-200 dark:bg-slate-700 w-10 h-6 rounded-full transition-colors"></div>
<div class="dot absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform transform translate-x-full"></div>
</div>
<div class="ml-3 text-sm font-medium text-slate-700 dark:text-slate-300">เปิดใช้งาน Account นี้</div>
</label>
</div>
</div>
</div>
<div class="bg-slate-50 dark:bg-slate-700/50 px-6 py-4 flex justify-end space-x-3">
<button type="button" onclick="document.getElementById('createUserModal').classList.add('hidden')" class="px-4 py-2 text-sm font-semibold text-slate-700 dark:text-slate-300 bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-600 rounded-xl hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors">ยกเลิก</button>
<button type="submit" class="px-4 py-2 text-sm font-semibold text-white bg-sky-500 rounded-xl hover:bg-sky-600 transition-colors">บันทึก</button>
</div>
</form>
</div>
</div>
</div>
<!-- Edit Modal -->
<div id="editUserModal" class="hidden fixed inset-0 z-50 overflow-y-auto">
<div class="fixed inset-0 bg-slate-900/50 backdrop-blur-sm transition-opacity" onclick="document.getElementById('editUserModal').classList.add('hidden')"></div>
<div class="flex min-h-screen items-center justify-center p-4">
<div class="relative transform overflow-hidden rounded-2xl bg-white dark:bg-slate-800 text-left shadow-xl transition-all sm:w-full sm:max-w-lg border border-slate-100 dark:border-slate-700">
<form action="<?= BASE_URL ?>/users/update" method="POST">
<input type="hidden" id="edit_id" name="id">
<div class="p-6">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-6">แก้ไขผู้ใช้งาน</h3>
<div class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label for="edit_first_name" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">ชื่อ (First Name)</label>
<input type="text" id="edit_first_name" name="first_name" required class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-sky-500 bg-slate-50 dark:bg-slate-900 sm:text-sm">
</div>
<div>
<label for="edit_last_name" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">นามสกุล (Last Name)</label>
<input type="text" id="edit_last_name" name="last_name" required class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-sky-500 bg-slate-50 dark:bg-slate-900 sm:text-sm">
</div>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">ชื่อผู้ใช้ (Username)</label>
<input type="text" id="edit_username" disabled class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-500 dark:text-slate-400 shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 bg-slate-100 dark:bg-slate-800/50 sm:text-sm cursor-not-allowed">
</div>
<div>
<label for="edit_password" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">รหัสผ่านใหม่ (ปล่อยว่างถ้าไม่ต้องการเปลี่ยน)</label>
<input type="password" id="edit_password" name="password" class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-sky-500 bg-slate-50 dark:bg-slate-900 sm:text-sm">
</div>
<div>
<label for="edit_role_id" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">สิทธิ์การใช้งาน (Role)</label>
<select id="edit_role_id" name="role_id" required class="block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 dark:text-white shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 focus:ring-2 focus:ring-sky-500 bg-slate-50 dark:bg-slate-900 sm:text-sm">
<?php foreach($roles as $role): ?>
<option value="<?= $role['id'] ?>"><?= htmlspecialchars($role['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
<div class="bg-slate-50 dark:bg-slate-700/50 px-6 py-4 flex justify-end space-x-3">
<button type="button" onclick="document.getElementById('editUserModal').classList.add('hidden')" class="px-4 py-2 text-sm font-semibold text-slate-700 dark:text-slate-300 bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-600 rounded-xl hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors">ยกเลิก</button>
<button type="submit" class="px-4 py-2 text-sm font-semibold text-white bg-sky-500 rounded-xl hover:bg-sky-600 transition-colors">บันทึกการแก้ไข</button>
</div>
</form>
</div>
</div>
</div>
<style>
/* Custom Toggle Switch Logic */
input:checked ~ .block { background-color: #0ea5e9; }
input:checked ~ .dot { transform: translateX(100%); }
input:not(:checked) ~ .dot { transform: translateX(0); }
</style>
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script>
function openEditModal(user) {
document.getElementById('edit_id').value = user.id;
document.getElementById('edit_first_name').value = user.first_name;
document.getElementById('edit_last_name').value = user.last_name;
document.getElementById('edit_username').value = user.username;
document.getElementById('edit_role_id').value = user.role_id;
document.getElementById('editUserModal').classList.remove('hidden');
}
$(document).ready(function() {
$('#hr_search').select2({
placeholder: 'ค้นหาเจ้าหน้าที่จากระบบ HR',
allowClear: true,
ajax: {
url: '<?= BASE_URL ?>/settings/system/search-officer',
dataType: 'json',
delay: 250,
data: function (params) {
return { q: params.term };
},
processResults: function (data) {
return { results: data.data || [] };
},
cache: true
}
});
$('#hr_search').on('select2:select', function (e) {
var data = e.params.data;
// Name format usually comes like "คำนำหน้า ชื่อ สกุล (ตำแหน่ง)"
// We'll extract basic names from the full string if we can't separate it easily,
// but the name returned in search-officer is "คำนำหน้า ชื่อ สกุล"
var parts = data.name.trim().split(' ');
if (parts.length >= 2) {
var fname = parts[0];
var lname = parts.slice(1).join(' ');
$('#first_name').val(fname.replace(/นาย|นางสาว|นาง|ว่าที่ร.ต.|พ.ต.ต.|ร.ต.อ./g, ''));
$('#last_name').val(lname);
} else {
$('#first_name').val(data.name);
}
$('#username').val(data.id); // ID is usually HR_CID
});
// Checkbox toggle prefix
$('#togglePrefix').on('change', function() {
if ($(this).is(':checked')) {
$('.name-prefix').removeClass('hidden');
} else {
$('.name-prefix').addClass('hidden');
}
});
});
</script>
<?php require_once APP_ROOT . '/app/Views/layouts/footer.php'; ?>
@@ -0,0 +1,154 @@
<?php require_once APP_ROOT . '/app/Views/layouts/header.php'; ?>
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-6 gap-4">
<div>
<h2 class="text-2xl font-bold text-slate-800 dark:text-white">ประวัติการใช้งานระบบ (Logs)</h2>
<p class="text-slate-500 dark:text-slate-400 text-sm mt-1">ตรวจสอบประวัติการเข้าใช้งานและการทำรายการต่างๆ ของผู้ใช้</p>
</div>
<a href="<?= BASE_URL ?>/users" class="inline-flex items-center justify-center px-4 py-2.5 bg-slate-100 dark:bg-slate-700 hover:bg-slate-200 dark:hover:bg-slate-600 text-slate-700 dark:text-slate-200 text-sm font-medium rounded-xl transition-colors shadow-sm w-full md:w-auto">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 15L3 9m0 0l6-6M3 9h12a6 6 0 010 12h-3" /></svg>
กลับไปจัดการผู้ใช้
</a>
</div>
<!-- Filters -->
<div class="bg-white dark:bg-slate-800 p-4 rounded-xl shadow-sm border border-slate-100 dark:border-slate-700 mb-6">
<form action="<?= BASE_URL ?>/users/logs" method="GET" class="flex flex-col md:flex-row gap-4">
<div class="flex-1">
<label class="block text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">ผู้ใช้งาน</label>
<select name="user_id" class="block w-full rounded-lg border-slate-200 dark:border-slate-600 bg-slate-50 dark:bg-slate-900/50 text-slate-700 dark:text-slate-300 text-sm py-2 px-3 focus:ring-sky-500">
<option value="">ทั้งหมด</option>
<?php foreach($users as $u): ?>
<option value="<?= $u['id'] ?>" <?= ($filters['user_id'] ?? '') == $u['id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($u['first_name'] . ' ' . $u['last_name'] . ' (' . $u['username'] . ')') ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="flex-1">
<label class="block text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">ประเภทรายการ</label>
<select name="action" class="block w-full rounded-lg border-slate-200 dark:border-slate-600 bg-slate-50 dark:bg-slate-900/50 text-slate-700 dark:text-slate-300 text-sm py-2 px-3 focus:ring-sky-500">
<option value="">ทั้งหมด</option>
<option value="LOGIN" <?= ($filters['action'] ?? '') == 'LOGIN' ? 'selected' : '' ?>>Login</option>
<option value="CREATE_USER" <?= ($filters['action'] ?? '') == 'CREATE_USER' ? 'selected' : '' ?>>เพิ่มผู้ใช้งาน</option>
<option value="UPDATE_USER" <?= ($filters['action'] ?? '') == 'UPDATE_USER' ? 'selected' : '' ?>>แก้ไขข้อมูลผู้ใช้</option>
<option value="TOGGLE_STATUS" <?= ($filters['action'] ?? '') == 'TOGGLE_STATUS' ? 'selected' : '' ?>>เปลี่ยนสถานะสิทธิ์</option>
<option value="UPDATE_SETTINGS" <?= ($filters['action'] ?? '') == 'UPDATE_SETTINGS' ? 'selected' : '' ?>>อัปเดตการตั้งค่าระบบ</option>
</select>
</div>
<div class="flex-1">
<label class="block text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">วันที่เริ่มต้น</label>
<input type="date" name="date_start" value="<?= htmlspecialchars($filters['date_start'] ?? '') ?>" class="block w-full rounded-lg border-slate-200 dark:border-slate-600 bg-slate-50 dark:bg-slate-900/50 text-slate-700 dark:text-slate-300 text-sm py-2 px-3 focus:ring-sky-500">
</div>
<div class="flex-1">
<label class="block text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">วันที่สิ้นสุด</label>
<input type="date" name="date_end" value="<?= htmlspecialchars($filters['date_end'] ?? '') ?>" class="block w-full rounded-lg border-slate-200 dark:border-slate-600 bg-slate-50 dark:bg-slate-900/50 text-slate-700 dark:text-slate-300 text-sm py-2 px-3 focus:ring-sky-500">
</div>
<div class="flex items-end">
<button type="submit" class="w-full md:w-auto px-4 py-2 bg-sky-500 hover:bg-sky-600 text-white text-sm font-medium rounded-lg transition-colors shadow-sm">
ค้นหา
</button>
<a href="<?= BASE_URL ?>/users/logs" class="ml-2 px-4 py-2 bg-slate-200 dark:bg-slate-700 hover:bg-slate-300 dark:hover:bg-slate-600 text-slate-700 dark:text-slate-300 text-sm font-medium rounded-lg transition-colors text-center">
ล้างค่า
</a>
</div>
</form>
</div>
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-100 dark:border-slate-700 overflow-hidden">
<div class="flex justify-end p-4 pb-0">
<label class="inline-flex items-center cursor-pointer">
<input type="checkbox" id="togglePrefix" class="w-4 h-4 text-indigo-600 bg-slate-100 border-slate-300 rounded focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:ring-offset-slate-800 focus:ring-2 dark:bg-slate-700 dark:border-slate-600">
<span class="ml-2 text-sm font-medium text-slate-700 dark:text-slate-300">แสดงคำนำหน้าชื่อ</span>
</label>
</div>
<div class="overflow-x-auto p-4 pt-2">
<table class="w-full text-left text-sm text-slate-600 dark:text-slate-300">
<thead class="bg-slate-50 dark:bg-slate-900/50 text-slate-500 dark:text-slate-400 text-xs uppercase font-semibold">
<tr>
<th class="px-6 py-4">วัน-เวลา</th>
<th class="px-6 py-4">ผู้ใช้งาน</th>
<th class="px-6 py-4">รายการ (Action)</th>
<th class="px-6 py-4">รายละเอียด</th>
<th class="px-6 py-4">IP Address</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 dark:divide-slate-700">
<?php if (empty($logs)): ?>
<tr>
<td colspan="5" class="px-6 py-8 text-center text-slate-500 dark:text-slate-400">
ไม่พบข้อมูลประวัติการใช้งานตามเงื่อนไขที่ค้นหา
</td>
</tr>
<?php else: ?>
<?php foreach ($logs as $log): ?>
<tr class="hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors">
<td class="px-6 py-4 whitespace-nowrap text-xs text-slate-500 dark:text-slate-400">
<?= date('d/m/Y H:i:s', strtotime($log['created_at'])) ?>
</td>
<td class="px-6 py-4" data-sort="<?= htmlspecialchars(preg_replace('/^(นาย|นางสาว|นาง|ว่าที่ร้อยตรี|น\.ส\.|นพ\.|พญ\.|ดร\.)\s*/u', '', trim($log['first_name']))) ?>">
<?php
$rawName = trim($log['first_name']);
$prefix = '';
$bodyName = $rawName;
if (preg_match('/^(นาย|นางสาว|นาง|ว่าที่ร้อยตรี|น\.ส\.|นพ\.|พญ\.|ดร\.)\s*(.*)$/u', $rawName, $matches)) {
$prefix = $matches[1];
$bodyName = $matches[2];
}
?>
<?php if ($log['user_id']): ?>
<div class="font-medium text-slate-800 dark:text-slate-200">
<span class="name-prefix hidden"><?= htmlspecialchars($prefix) ?></span><?= htmlspecialchars($bodyName . ' ' . $log['last_name']) ?>
</div>
<div class="text-xs text-slate-500">@<?= htmlspecialchars($log['username']) ?></div>
<?php else: ?>
<div class="font-medium text-sky-600 dark:text-sky-400">ตู้ Kiosk</div>
<div class="text-xs text-slate-500">Public Access</div>
<?php endif; ?>
</td>
<td class="px-6 py-4">
<?php
$actionColors = [
'LOGIN' => 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400',
'CREATE_USER' => 'bg-sky-100 text-sky-700 dark:bg-sky-900/30 dark:text-sky-400',
'UPDATE_USER' => 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400',
'TOGGLE_STATUS' => 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400',
'UPDATE_SETTINGS' => 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-400',
'KIOSK_PRINT_SLIP' => 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',
'KIOSK_PRINT_TAX' => 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400'
];
$color = $actionColors[$log['action']] ?? 'bg-slate-100 text-slate-700 dark:bg-slate-700 dark:text-slate-300';
?>
<span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold <?= $color ?>">
<?= htmlspecialchars($log['action']) ?>
</span>
</td>
<td class="px-6 py-4 text-sm text-slate-600 dark:text-slate-400 max-w-md truncate" title="<?= htmlspecialchars($log['description']) ?>">
<?= htmlspecialchars($log['description']) ?>
</td>
<td class="px-6 py-4 text-xs font-mono text-slate-500">
<?= htmlspecialchars($log['ip_address']) ?>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script>
$(document).ready(function() {
// Checkbox toggle prefix
$('#togglePrefix').on('change', function() {
if ($(this).is(':checked')) {
$('.name-prefix').removeClass('hidden');
} else {
$('.name-prefix').addClass('hidden');
}
});
});
</script>
<?php require_once APP_ROOT . '/app/Views/layouts/footer.php'; ?>