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,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]);
}
}
?>