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,5 @@
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_URI} !/public/ [NC]
RewriteRule ^(.*)$ public/$1 [L]
</IfModule>
@@ -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'; ?>
@@ -0,0 +1,26 @@
<?php
/**
* Application Configuration
*/
// Detect the base URL dynamically or set it manually for production
// For local: http://localhost/จัดการข้อมูลเงินเดือนเจ้าหน้าที่โรงพยาบาล
// For prod: https://intra1.samuihospital.go.th/intranet/sub_system/fn_payroll
$is_https = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on';
$protocol = $is_https ? "https://" : "http://";
$host = $_SERVER['HTTP_HOST'];
$script_dirname = dirname($_SERVER['SCRIPT_NAME']);
// Remove '/public' from the end of the script dirname if it exists
$base_dir = preg_replace('/\/public$/', '', $script_dirname);
if ($base_dir === '\\' || $base_dir === '/') {
$base_dir = '';
}
$base_url = $protocol . $host . $base_dir;
define('BASE_URL', $base_url);
define('APP_NAME', 'ระบบจัดการข้อมูลเงินเดือนเจ้าหน้าที่โรงพยาบาล');
define('APP_VERSION', '1.0.0');
?>
@@ -0,0 +1,68 @@
<?php
/**
* Database Configuration
*/
define('DB_HOST', 'localhost');
define('DB_PORT', '3306');
define('DB_NAME', 'ksh_payroll');
define('DB_USER', 'root');
define('DB_PASS', '@Samui@10742');
define('DB_CHARSET', 'utf8mb4');
// HR Database (hosoffice_2566) Connection Details
define('HR_DB_HOST', '10.0.250.115'); // <-- เปลี่ยนเป็น IP ของ HR Server
define('HR_DB_PORT', '3306');
define('HR_DB_NAME', 'hosoffice_2566');
define('HR_DB_USER', 'hosoffice'); // <-- เปลี่ยนเป็น Username ของ HR
define('HR_DB_PASS', 'hosoffice10742'); // <-- เปลี่ยนเป็น Password ของ HR
define('HR_DB_CHARSET', 'utf8mb4'); // หรือ utf8mb4 ขึ้นอยู่กับฐานข้อมูล HR
class Database
{
private static $instance = null;
private static $hrInstance = null;
private $pdo;
private function __construct($type = 'default')
{
if ($type === 'hr') {
$dsn = "mysql:host=" . HR_DB_HOST . ";port=" . HR_DB_PORT . ";dbname=" . HR_DB_NAME . ";charset=" . HR_DB_CHARSET;
$user = HR_DB_USER;
$pass = HR_DB_PASS;
} else {
$dsn = "mysql:host=" . DB_HOST . ";port=" . DB_PORT . ";dbname=" . DB_NAME . ";charset=" . DB_CHARSET;
$user = DB_USER;
$pass = DB_PASS;
}
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$this->pdo = new PDO($dsn, $user, $pass, $options);
} catch (\PDOException $e) {
throw new \PDOException($e->getMessage(), (int)$e->getCode());
}
}
public static function getInstance()
{
if (self::$instance === null) {
self::$instance = new self('default');
}
return self::$instance->pdo;
}
public static function getHrConnection()
{
if (self::$hrInstance === null) {
self::$hrInstance = new self('hr');
}
return self::$hrInstance->pdo;
}
}
@@ -0,0 +1,295 @@
-- Hospital Payroll Management System - Database Schema V1.0 & V2.0
CREATE DATABASE IF NOT EXISTS hospital_payroll CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE hospital_payroll;
-- ==========================================
-- 1. Authentication & Authorization (RBAC)
-- ==========================================
CREATE TABLE roles (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL UNIQUE,
description VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE permissions (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL UNIQUE,
description VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE role_permissions (
role_id INT NOT NULL,
permission_id INT NOT NULL,
PRIMARY KEY (role_id, permission_id),
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
);
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
role_id INT NOT NULL,
employee_id VARCHAR(50) NULL,
email VARCHAR(100),
first_name VARCHAR(100),
last_name VARCHAR(100),
status ENUM('active', 'inactive') DEFAULT 'active',
last_login TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE RESTRICT
);
-- ==========================================
-- 2. Master Settings (Income & Deduction)
-- ==========================================
CREATE TABLE income_master (
id INT AUTO_INCREMENT PRIMARY KEY,
code VARCHAR(20) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
description TEXT,
display_order INT DEFAULT 0,
color_code VARCHAR(20),
icon_class VARCHAR(50),
is_taxable BOOLEAN DEFAULT TRUE,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE deduction_master (
id INT AUTO_INCREMENT PRIMARY KEY,
code VARCHAR(20) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
description TEXT,
display_order INT DEFAULT 0,
color_code VARCHAR(20),
icon_class VARCHAR(50),
is_tax_deductible BOOLEAN DEFAULT FALSE,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- ==========================================
-- 3. Payroll Management
-- ==========================================
CREATE TABLE salary_year (
id INT AUTO_INCREMENT PRIMARY KEY,
year_no INT NOT NULL UNIQUE,
status ENUM('open', 'closed') DEFAULT 'open',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE salary_month (
id INT AUTO_INCREMENT PRIMARY KEY,
year_id INT NOT NULL,
month_no INT NOT NULL,
status ENUM('Draft', 'Imported', 'Pending Review', 'HR Approved', 'Finance Approved', 'Director Approved', 'Locked', 'Published', 'Cancelled') DEFAULT 'Draft',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE(year_id, month_no),
FOREIGN KEY (year_id) REFERENCES salary_year(id) ON DELETE CASCADE
);
CREATE TABLE employee_salary (
id INT AUTO_INCREMENT PRIMARY KEY,
salary_month_id INT NOT NULL,
employee_code VARCHAR(50) NOT NULL,
national_id VARCHAR(20) NOT NULL,
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
position VARCHAR(100),
department VARCHAR(100),
base_salary DECIMAL(10,2) DEFAULT 0.00,
total_income DECIMAL(10,2) DEFAULT 0.00,
total_deduction DECIMAL(10,2) DEFAULT 0.00,
net_salary DECIMAL(10,2) DEFAULT 0.00,
remark TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE(salary_month_id, employee_code),
FOREIGN KEY (salary_month_id) REFERENCES salary_month(id) ON DELETE CASCADE
);
CREATE TABLE salary_income (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
employee_salary_id INT NOT NULL,
income_master_id INT NOT NULL,
amount DECIMAL(10,2) NOT NULL DEFAULT 0.00,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (employee_salary_id) REFERENCES employee_salary(id) ON DELETE CASCADE,
FOREIGN KEY (income_master_id) REFERENCES income_master(id) ON DELETE RESTRICT
);
CREATE TABLE salary_deduction (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
employee_salary_id INT NOT NULL,
deduction_master_id INT NOT NULL,
amount DECIMAL(10,2) NOT NULL DEFAULT 0.00,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (employee_salary_id) REFERENCES employee_salary(id) ON DELETE CASCADE,
FOREIGN KEY (deduction_master_id) REFERENCES deduction_master(id) ON DELETE RESTRICT
);
-- ==========================================
-- 4. Import Data Logging
-- ==========================================
CREATE TABLE import_history (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
salary_month_id INT NOT NULL,
file_name VARCHAR(255),
status ENUM('success', 'failed', 'partial') DEFAULT 'success',
total_records INT DEFAULT 0,
success_records INT DEFAULT 0,
error_records INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE RESTRICT,
FOREIGN KEY (salary_month_id) REFERENCES salary_month(id) ON DELETE CASCADE
);
CREATE TABLE import_detail (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
import_history_id INT NOT NULL,
line_number INT,
national_id VARCHAR(20),
error_message TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (import_history_id) REFERENCES import_history(id) ON DELETE CASCADE
);
-- ==========================================
-- 5. Logging and Auditing
-- ==========================================
CREATE TABLE audit_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id INT NULL,
action VARCHAR(100) NOT NULL,
table_name VARCHAR(100),
record_id INT,
old_value JSON,
new_value JSON,
ip_address VARCHAR(45),
user_agent VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
);
CREATE TABLE salary_slip_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id INT NULL,
employee_salary_id INT NOT NULL,
action ENUM('view', 'print', 'download', 'email') NOT NULL,
ip_address VARCHAR(45),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (employee_salary_id) REFERENCES employee_salary(id) ON DELETE CASCADE
);
CREATE TABLE report_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
report_name VARCHAR(100) NOT NULL,
parameters JSON,
ip_address VARCHAR(45),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- ==========================================
-- 6. Tax Certificates
-- ==========================================
CREATE TABLE tax_certificate (
id INT AUTO_INCREMENT PRIMARY KEY,
year_id INT NOT NULL,
employee_code VARCHAR(50) NOT NULL,
total_income DECIMAL(12,2) DEFAULT 0.00,
total_tax DECIMAL(12,2) DEFAULT 0.00,
total_provident_fund DECIMAL(12,2) DEFAULT 0.00,
total_social_security DECIMAL(12,2) DEFAULT 0.00,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (year_id) REFERENCES salary_year(id) ON DELETE CASCADE
);
CREATE TABLE tax_certificate_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id INT NULL,
tax_certificate_id INT NOT NULL,
action ENUM('view', 'print', 'download') NOT NULL,
ip_address VARCHAR(45),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (tax_certificate_id) REFERENCES tax_certificate(id) ON DELETE CASCADE
);
-- ==========================================
-- 7. File Attachments
-- ==========================================
CREATE TABLE attachments (
id INT AUTO_INCREMENT PRIMARY KEY,
related_table VARCHAR(50),
related_id INT,
file_name VARCHAR(255) NOT NULL,
file_path VARCHAR(255) NOT NULL,
file_type VARCHAR(100),
file_size INT,
uploaded_by INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (uploaded_by) REFERENCES users(id) ON DELETE SET NULL
);
-- ==========================================
-- 8. System Integrations & Settings
-- ==========================================
CREATE TABLE system_setting (
id INT AUTO_INCREMENT PRIMARY KEY,
setting_key VARCHAR(100) NOT NULL UNIQUE,
setting_value TEXT,
description VARCHAR(255),
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE external_database (
id INT AUTO_INCREMENT PRIMARY KEY,
connection_name VARCHAR(100) NOT NULL UNIQUE,
host VARCHAR(100) NOT NULL,
port INT DEFAULT 3306,
db_name VARCHAR(100) NOT NULL,
db_user VARCHAR(100) NOT NULL,
db_password VARCHAR(255),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE backup_history (
id INT AUTO_INCREMENT PRIMARY KEY,
file_name VARCHAR(255) NOT NULL,
file_size INT NOT NULL,
status ENUM('success', 'failed') DEFAULT 'success',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- ==========================================
-- Default Data Inserts
-- ==========================================
INSERT INTO roles (name, description) VALUES
('Administrator', 'Full system access'),
('HR', 'Human Resources access'),
('Finance', 'Financial review access'),
('Director', 'Final approval access'),
('Auditor', 'Read-only access for auditing');
-- admin / admin123
INSERT INTO users (username, password_hash, role_id, first_name, last_name)
VALUES ('admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 1, 'System', 'Administrator');
@@ -0,0 +1,63 @@
-- Script สำหรับนำเข้าข้อมูล Master Data ของรายการรับและจ่ายโรงพยาบาล
-- สามารถนำไปรันใน phpMyAdmin หรือโปรแกรมจัดการฐานข้อมูลได้เลย
-- 1. ล้างข้อมูลเก่า (ถ้าต้องการลบของเก่าออกก่อนรันใหม่)
-- TRUNCATE TABLE salary_income;
-- TRUNCATE TABLE salary_deduction;
-- DELETE FROM income_master;
-- DELETE FROM deduction_master;
-- ALTER TABLE income_master AUTO_INCREMENT = 1;
-- ALTER TABLE deduction_master AUTO_INCREMENT = 1;
-- 2. นำเข้ารายการรับ (Income)
INSERT IGNORE INTO income_master (code, name, display_order) VALUES
('SALARY', 'เงินเดือน', 1),
('FIX_EMP', 'ค่าจ้างประจำ', 2),
('TEMP_EMP', 'ค่าจ้างชั่วคราว', 3),
('SUMSAL_LAT', 'เงินเดือน/ปจต./ตอบแทน ตกเบิก', 4),
('MONT_FEE', 'ค่าตอบแทนรายเดือน', 5),
('TEMP_EARN', 'เงินช่วยค่าครองชีพ', 6),
('HOUSE_RENT', 'ค่าตอบแทนเจ้าหน้าที่', 7),
('NO_PRIVATE', 'ค่าตอบแทนไม่ประกอบเวชฯ', 8),
('P_T_S', 'ค่าตอบแทน พตส./คตส.', 9),
('P_T_S_LATE', 'ค่าตอบแทน พตส. ตกเบิก', 10),
('SUMOT', 'ค่าล่วงเวลา เงินบำรุง', 11),
('SUMOTSSN', 'ค่าล่วงเวลา เงินประกันสังคม', 12),
('OTREFER', 'ค่าล่วงเวลา Refer', 13),
('TREAT', 'ค่ารักษาพยาบาล', 14),
('EDUCATE', 'ค่าเล่าเรียนบุตร', 15),
('OTHEROT1', 'ค่าตอบแทน P4P', 16),
('OFFICERFEE', 'ค่าตอบแทน ฉ.11', 17),
('WORK_FEE', 'ค่าตอบแทนอื่นๆ (1)', 18),
('OTHEROT2', 'ค่าตอบแทนอื่นๆ (2)', 19),
('POS_FEE', 'รายรับอื่นๆ (1)', 20),
('EXTR_FEE', 'รายรับอื่นๆ (2)', 21),
('OTHERREC_1', 'รายรับอื่นๆ (3)', 22),
('OTHERREC_2', 'รายรับอื่นๆ (4)', 23),
('EARNLATE', 'รายรับอื่นๆ (5)', 24);
-- 3. นำเข้ารายการจ่าย (Deduction)
INSERT IGNORE INTO deduction_master (code, name, display_order) VALUES
('FUND_A', 'ประกันสังคม', 1),
('FUND_B', 'ประกันสังคมหักตกเบิก', 2),
('TAX', 'ภาษี', 3),
('FENERAL', 'ฌกส.', 4),
('BUILDING', 'ธ.กรุงไทย/ธ.ธอส', 5),
('WATER_A', 'ค่าน้ำ หักเงินเดือน', 6),
('WATER_B', 'ค่าน้ำ หักโอที', 7),
('ELECT', 'ค่าไฟ หักเงินเดือน', 8),
('ELECTRIC', 'ค่าไฟ หักโอที', 9),
('DOCOFFICE', 'ค่าสวัสดิการองค์กรแพทย์', 10),
('DOCTOR', 'ค่าสวัสดิการแพทย์ หักโอที', 11),
('CO_OP_A', 'สหกรณ์ หักเงินเดือน', 12),
('OTHEREXP_1', 'กสล./พกส.', 13),
('CO_OP_B', 'กสล./พกส. หักตกเบิก', 14),
('SAVING_A', 'ธ.ออมสิน หักเงินเดือน', 15),
('SAVING_B', 'ธ.ออมสิน หักโอที', 16),
('MON_BACK_A', 'คืนเงิน รพ.', 17),
('MON_BACK_B', 'คืนเงินบำรุง หักตกเบิก', 18),
('CLEANER', 'ค่าทำความสะอาดแฟลต', 19),
('CO_OP1', 'รายจ่ายอื่นๆ (1)', 20),
('CABLE', 'รายจ่ายอื่นๆ (2)', 21),
('OTHEREXP_2', 'รายจ่ายอื่นๆ (3)', 22),
('OTHEREXP', 'รายจ่ายอื่นๆ (4)', 23);
@@ -0,0 +1,8 @@
<IfModule mod_rewrite.c>
RewriteEngine On
# Send all requests to index.php if they are not a real file or directory
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]
</IfModule>
@@ -0,0 +1,94 @@
/* Modern Hospital Theme - Glassmorphism & Gradient */
:root {
--primary-color: #0d6efd;
--secondary-color: #6c757d;
--background-start: #e0eafc;
--background-end: #cfdef3;
--glass-bg: rgba(255, 255, 255, 0.25);
--glass-border: rgba(255, 255, 255, 0.18);
--glass-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
}
body {
background: linear-gradient(135deg, var(--background-start) 0%, var(--background-end) 100%);
min-height: 100vh;
font-family: 'Inter', sans-serif; /* Recommended modern font */
}
/* Glassmorphism Card Utility */
.glass-card {
background: var(--glass-bg);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid var(--glass-border);
box-shadow: var(--glass-shadow);
border-radius: 15px;
padding: 20px;
}
/* Loading button animation */
.btn-loading {
position: relative;
pointer-events: none;
opacity: 0.8;
}
.btn-loading::after {
content: '';
position: absolute;
width: 1rem;
height: 1rem;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
border: 2px solid transparent;
border-top-color: currentColor;
border-radius: 50%;
animation: button-loading-spinner 1s ease infinite;
}
@keyframes button-loading-spinner {
from { transform: rotate(0turn); }
to { transform: rotate(1turn); }
}
/* Sidebar & Wrapper Layout */
#wrapper {
overflow-x: hidden;
min-height: 100vh;
}
.glass-sidebar {
min-height: 100vh;
width: 250px;
background: rgba(255, 255, 255, 0.4);
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
border-right: 1px solid var(--glass-border);
transition: margin 0.25s ease-out;
}
.glass-sidebar .list-group-item {
transition: all 0.2s ease;
}
.glass-sidebar .list-group-item:hover, .active-menu {
background: rgba(13, 110, 253, 0.1) !important;
color: var(--primary-color) !important;
transform: translateX(5px);
}
#page-content-wrapper {
min-width: 100vw;
}
@media (min-width: 992px) {
#page-content-wrapper {
min-width: 0;
width: 100%;
}
}
#wrapper.toggled #sidebar-wrapper {
margin-left: -250px;
}
@@ -0,0 +1,24 @@
/**
* Main JavaScript File for Hospital Payroll Management System
*/
document.addEventListener('DOMContentLoaded', () => {
console.log('Hospital Payroll Management System - Initialized');
// Setup global CSRF token for AJAX requests if available
const csrfToken = document.querySelector('meta[name="csrf-token"]');
if (csrfToken) {
// Axios or fetch interceptor can be setup here
}
});
/**
* Utility to toggle loading state on buttons
*/
function toggleButtonLoading(buttonElement, isLoading) {
if (isLoading) {
buttonElement.classList.add('btn-loading');
} else {
buttonElement.classList.remove('btn-loading');
}
}
@@ -0,0 +1,232 @@
<?php
session_start();
/**
* Entry Point for Hospital Payroll Management System
*/
// Define application root
define('APP_ROOT', dirname(__DIR__));
// Require basic configuration
require_once APP_ROOT . '/config/app.php';
require_once APP_ROOT . '/config/database.php';
// Very basic autoloader for our MVC structure
spl_autoload_register(function ($className) {
$path = APP_ROOT . '/' . str_replace('\\', '/', $className) . '.php';
if (file_exists($path)) {
require_once $path;
}
});
// Simple Router
$requestUri = $_SERVER['REQUEST_URI'];
$requestMethod = $_SERVER['REQUEST_METHOD'];
// Remove the base directory from the request URI to get the relative path
$baseDir = parse_url(BASE_URL, PHP_URL_PATH) ?? '';
if ($baseDir !== '' && strpos($requestUri, $baseDir) === 0) {
$uri = substr($requestUri, strlen($baseDir));
} else {
$uri = $requestUri;
}
$uri = strtok($uri, '?'); // Remove query string
$uri = trim($uri, '/');
$controllerName = 'app\\Controllers\\AuthController';
$methodName = 'index';
if ($uri === '' || $uri === 'login') {
if ($requestMethod === 'POST') {
$controllerName = 'app\\Controllers\\AuthController';
$methodName = 'login';
} else {
$controllerName = 'app\\Controllers\\AuthController';
$methodName = 'index';
}
} elseif ($uri === 'logout') {
$controllerName = 'app\\Controllers\\AuthController';
$methodName = 'logout';
} elseif ($uri === 'dashboard') {
$controllerName = 'app\\Controllers\\DashboardController';
$methodName = 'index';
} elseif ($uri === 'payroll') {
$controllerName = 'app\\Controllers\\PayrollController';
$methodName = 'index';
} elseif ($uri === 'payroll/store') {
$controllerName = 'app\\Controllers\\PayrollController';
$methodName = 'store';
} elseif ($uri === 'payroll/clear') {
$controllerName = 'app\\Controllers\\PayrollController';
$methodName = 'clearData';
} elseif (preg_match('#^payroll/details(?:/(\d+))?$#', $uri, $matches)) {
$_GET['id'] = $matches[1] ?? ($_GET['id'] ?? null);
$controllerName = 'app\\Controllers\\PayrollController';
$methodName = 'details';
} elseif ($uri === 'payroll/copy-previous') {
$controllerName = 'app\\Controllers\\PayrollController';
$methodName = 'copyPreviousMonth';
} elseif ($uri === 'payroll/employee/add') {
$controllerName = 'app\\Controllers\\PayrollController';
$methodName = 'addEmployee';
} elseif ($uri === 'payroll/employee/delete') {
$controllerName = 'app\\Controllers\\PayrollController';
$methodName = 'deleteEmployee';
} elseif (preg_match('#^payroll/employee/edit(?:/(\d+))?$#', $uri, $matches)) {
$_GET['id'] = $matches[1] ?? ($_GET['id'] ?? null);
$controllerName = 'app\\Controllers\\PayrollController';
$methodName = 'editEmployee';
} elseif ($uri === 'payroll/saveEmployee') {
$controllerName = 'app\\Controllers\\PayrollController';
$methodName = 'saveEmployee';
} elseif ($uri === 'payroll/updateStatus' && $_SERVER['REQUEST_METHOD'] === 'POST') {
$controllerName = 'app\\Controllers\\PayrollController';
$methodName = 'updateStatus';
} elseif (preg_match('#^payroll/deleteEmployee/(\d+)$#', $uri, $matches)) {
$_GET['id'] = $matches[1] ?? ($_GET['id'] ?? null);
$controllerName = 'app\\Controllers\\PayrollController';
$methodName = 'editEmployee';
} elseif ($uri === 'payroll/employee/update') {
$controllerName = 'app\\Controllers\\PayrollController';
$methodName = 'updateEmployee';
} elseif (preg_match('#^employee/image(?:/([\w\d_-]+))?$#', $uri, $matches)) {
$_GET['national_id'] = $matches[1] ?? null;
$controllerName = 'app\\Controllers\\HrController';
$methodName = 'image';
} elseif ($uri === 'settings/income') {
$controllerName = 'app\\Controllers\\SettingController';
if ($requestMethod === 'POST') {
$methodName = 'storeIncome';
} else {
$methodName = 'income';
}
} elseif ($uri === 'settings/income/update') {
$controllerName = 'app\\Controllers\\SettingController';
$methodName = 'updateIncome';
} elseif ($uri === 'settings/deduction') {
$controllerName = 'app\\Controllers\\SettingController';
if ($requestMethod === 'POST') {
$methodName = 'storeDeduction';
} else {
$methodName = 'deduction';
}
} elseif ($uri === 'settings/deduction/update') {
$controllerName = 'app\\Controllers\\SettingController';
$methodName = 'updateDeduction';
} elseif ($uri === 'settings/officer-types') {
$controllerName = 'app\\Controllers\\SettingController';
if ($requestMethod === 'POST') {
$methodName = 'storeOfficerType';
} else {
$methodName = 'officerTypes';
}
} elseif ($uri === 'settings/officer-types/update') {
$controllerName = 'app\\Controllers\\SettingController';
$methodName = 'updateOfficerType';
} elseif ($uri === 'settings/officer-types/toggle') {
$controllerName = 'app\\Controllers\\SettingController';
$methodName = 'toggleOfficerTypeStatus';
} elseif ($uri === 'settings/officer-types/delete') {
$controllerName = 'app\\Controllers\\SettingController';
$methodName = 'deleteOfficerType';
} elseif ($uri === 'settings/system/search-officer') {
$controllerName = 'app\\Controllers\\SettingController';
$methodName = 'searchOfficer';
} elseif ($uri === 'settings/system/save') {
$controllerName = 'app\\Controllers\\SettingController';
$methodName = 'saveSystemSettings';
} elseif ($uri === 'settings/system/upload-tax-signature') {
$controllerName = 'app\\Controllers\\SettingController';
$methodName = 'uploadTaxSignature';
} elseif ($uri === 'settings/system') {
$controllerName = 'app\\Controllers\\SettingController';
if ($requestMethod === 'POST') {
$methodName = 'uploadSignature';
} else {
$methodName = 'system';
}
} elseif ($uri === 'users') {
$controllerName = 'app\\Controllers\\UserController';
if ($requestMethod === 'POST') {
$methodName = 'store';
} else {
$methodName = 'index';
}
} elseif ($uri === 'users/update') {
$controllerName = 'app\\Controllers\\UserController';
$methodName = 'update';
} elseif ($uri === 'users/toggle_status') {
$controllerName = 'app\\Controllers\\UserController';
$methodName = 'toggle_status';
} elseif ($uri === 'users/logs') {
$controllerName = 'app\\Controllers\\UserController';
$methodName = 'logs';
} elseif ($uri === 'profile') {
$controllerName = 'app\\Controllers\\ProfileController';
$methodName = 'index';
} elseif ($uri === 'profile/update-password') {
$controllerName = 'app\\Controllers\\ProfileController';
$methodName = 'updatePassword';
} elseif ($uri === 'manual') {
$controllerName = 'app\\Controllers\\ManualController';
$methodName = 'index';
} elseif ($uri === 'reports') {
$controllerName = 'app\\Controllers\\ReportController';
$methodName = 'index';
} elseif (preg_match('#^reports/slip/([\w\d_-]+)/(\d{2})/(\d{4})$#', $uri, $matches)) {
$_GET['national_id'] = $matches[1];
$_GET['month'] = $matches[2];
$_GET['year'] = $matches[3];
$controllerName = 'app\\Controllers\\ReportController';
$methodName = 'printSlip';
} elseif ($uri === 'reports/tax-bulk') {
$controllerName = 'app\\Controllers\\ReportController';
$methodName = 'printTaxBulk';
} elseif ($uri === 'reports/salary') {
$controllerName = 'app\\Controllers\\ReportController';
$methodName = 'salaryReport';
} elseif ($uri === 'reports/salary/export') {
$controllerName = 'app\\Controllers\\ReportController';
$methodName = 'exportExcel';
} elseif (preg_match('#^reports/tax/([\w\d_-]+)/(\d{4})$#', $uri, $matches)) {
$_GET['national_id'] = $matches[1];
$_GET['year'] = $matches[2];
$controllerName = 'app\\Controllers\\ReportController';
$methodName = 'printTax';
} elseif ($uri === 'import') {
$controllerName = 'app\\Controllers\\ImportController';
$methodName = 'index';
} elseif ($uri === 'import/upload') {
$controllerName = 'app\\Controllers\\ImportController';
$methodName = 'upload';
} elseif ($uri === 'kiosk') {
$controllerName = 'app\\Controllers\\KioskController';
$methodName = 'index';
} elseif ($uri === 'kiosk/search') {
$controllerName = 'app\\Controllers\\KioskController';
$methodName = 'search';
} elseif (preg_match('#^kiosk/print/slip/([\w\d_-]+)/(\d{2})/(\d{4})$#', $uri, $matches)) {
$_GET['national_id'] = $matches[1];
$_GET['month'] = $matches[2];
$_GET['year'] = $matches[3];
$controllerName = 'app\\Controllers\\KioskController';
$methodName = 'printSlip';
} elseif ($uri === 'kiosk/print/slipBulk') {
$controllerName = 'app\\Controllers\\KioskController';
$methodName = 'printSlipBulk';
} elseif (preg_match('#^kiosk/print/tax/([\w\d_-]+)/(\d{4})$#', $uri, $matches)) {
$_GET['national_id'] = $matches[1];
$_GET['year'] = $matches[2];
$controllerName = 'app\\Controllers\\KioskController';
$methodName = 'printTax';
} else {
// Basic 404
http_response_code(404);
echo "<h1>404 Not Found</h1>";
echo "<p>The page you are looking for does not exist.</p>";
exit;
}
$controller = new $controllerName();
$controller->$methodName();
?>
@@ -0,0 +1,21 @@
{
"name": "Samui Hospital Payroll",
"short_name": "Payroll",
"description": "ระบบจัดการข้อมูลเงินเดือนเจ้าหน้าที่โรงพยาบาลเกาะสมุย",
"start_url": "/dashboard",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#0ea5e9",
"icons": [
{
"src": "assets/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "assets/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
@@ -0,0 +1,43 @@
const CACHE_NAME = 'payroll-app-v1';
const urlsToCache = [
'/',
'/public/assets/css/style.css',
'/public/assets/js/main.js'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
return cache.addAll(urlsToCache);
})
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// Cache hit - return response
if (response) {
return response;
}
return fetch(event.request);
})
);
});
self.addEventListener('activate', event => {
const cacheWhitelist = [CACHE_NAME];
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName);
}
})
);
})
);
});
@@ -0,0 +1,14 @@
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
);
INSERT IGNORE INTO officer_types (name, is_active) VALUES
('ข้าราชการ', 1),
('ลูกจ้างประจำ', 1),
('พนักงานราชการ', 1),
('พนักงานกระทรวงสาธารณสุข', 1),
('ลูกจ้างชั่วคราว', 1),
('ลูกจ้างรายวัน', 1);
@@ -0,0 +1,10 @@
<?php
try {
$db = new PDO("mysql:host=127.0.0.1;port=3306;dbname=ksh_payroll;charset=utf8mb4", "root", "@Samui@10742");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$sql = file_get_contents(__DIR__ . '/add_officer_types.sql');
$db->exec($sql);
echo "SQL executed successfully.\n";
} catch (\PDOException $e) {
echo "Error: " . $e->getMessage() . "\n";
}