diff --git a/db/migrations/20260217_hr_module.sql b/db/migrations/20260217_hr_module.sql new file mode 100644 index 0000000..1e7b8ef --- /dev/null +++ b/db/migrations/20260217_hr_module.sql @@ -0,0 +1,47 @@ +-- HR Module Migration +CREATE TABLE IF NOT EXISTS hr_departments ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS hr_employees ( + id INT AUTO_INCREMENT PRIMARY KEY, + department_id INT, + name VARCHAR(255) NOT NULL, + email VARCHAR(255), + phone VARCHAR(20), + position VARCHAR(100), + salary DECIMAL(15, 3) DEFAULT 0.000, + joining_date DATE, + status ENUM('active', 'inactive') DEFAULT 'active', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (department_id) REFERENCES hr_departments(id) ON DELETE SET NULL +); + +CREATE TABLE IF NOT EXISTS hr_attendance ( + id INT AUTO_INCREMENT PRIMARY KEY, + employee_id INT NOT NULL, + attendance_date DATE NOT NULL, + clock_in TIME, + clock_out TIME, + status ENUM('present', 'absent', 'on_leave') DEFAULT 'present', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (employee_id) REFERENCES hr_employees(id) ON DELETE CASCADE, + UNIQUE KEY (employee_id, attendance_date) +); + +CREATE TABLE IF NOT EXISTS hr_payroll ( + id INT AUTO_INCREMENT PRIMARY KEY, + employee_id INT NOT NULL, + payroll_month INT NOT NULL, + payroll_year INT NOT NULL, + basic_salary DECIMAL(15, 3) DEFAULT 0.000, + bonus DECIMAL(15, 3) DEFAULT 0.000, + deductions DECIMAL(15, 3) DEFAULT 0.000, + net_salary DECIMAL(15, 3) DEFAULT 0.000, + payment_date DATE, + status ENUM('pending', 'paid') DEFAULT 'pending', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (employee_id) REFERENCES hr_employees(id) ON DELETE CASCADE +); diff --git a/db/migrations/20260217_hr_payroll_unique.sql b/db/migrations/20260217_hr_payroll_unique.sql new file mode 100644 index 0000000..c7f5fd6 --- /dev/null +++ b/db/migrations/20260217_hr_payroll_unique.sql @@ -0,0 +1,2 @@ +-- Add unique constraint to payroll to prevent duplicates +ALTER TABLE hr_payroll ADD UNIQUE KEY employee_month_year (employee_id, payroll_month, payroll_year); diff --git a/index.php b/index.php index d6f2da0..430bd03 100644 --- a/index.php +++ b/index.php @@ -1219,6 +1219,126 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } } +// --- HR Handlers --- +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + if (isset($_POST['add_hr_department'])) { + $name = $_POST['name'] ?? ''; + if ($name) { + $stmt = db()->prepare("INSERT INTO hr_departments (name) VALUES (?)"); + $stmt->execute([$name]); + $message = "Department added successfully!"; + } + } + if (isset($_POST['edit_hr_department'])) { + $id = (int)$_POST['id']; + $name = $_POST['name'] ?? ''; + if ($id && $name) { + $stmt = db()->prepare("UPDATE hr_departments SET name = ? WHERE id = ?"); + $stmt->execute([$name, $id]); + $message = "Department updated successfully!"; + } + } + if (isset($_POST['delete_hr_department'])) { + $id = (int)$_POST['id']; + if ($id) { + $stmt = db()->prepare("DELETE FROM hr_departments WHERE id = ?"); + $stmt->execute([$id]); + $message = "Department deleted successfully!"; + } + } + if (isset($_POST['add_hr_employee'])) { + $dept_id = (int)$_POST['department_id'] ?: null; + $name = $_POST['name'] ?? ''; + $email = $_POST['email'] ?? ''; + $phone = $_POST['phone'] ?? ''; + $pos = $_POST['position'] ?? ''; + $salary = (float)($_POST['salary'] ?? 0); + $j_date = $_POST['joining_date'] ?: null; + if ($name) { + $stmt = db()->prepare("INSERT INTO hr_employees (department_id, name, email, phone, position, salary, joining_date) VALUES (?, ?, ?, ?, ?, ?, ?)"); + $stmt->execute([$dept_id, $name, $email, $phone, $pos, $salary, $j_date]); + $message = "Employee added successfully!"; + } + } + if (isset($_POST['edit_hr_employee'])) { + $id = (int)$_POST['id']; + $dept_id = (int)$_POST['department_id'] ?: null; + $name = $_POST['name'] ?? ''; + $email = $_POST['email'] ?? ''; + $phone = $_POST['phone'] ?? ''; + $pos = $_POST['position'] ?? ''; + $salary = (float)($_POST['salary'] ?? 0); + $j_date = $_POST['joining_date'] ?: null; + $status = $_POST['status'] ?? 'active'; + if ($id && $name) { + $stmt = db()->prepare("UPDATE hr_employees SET department_id = ?, name = ?, email = ?, phone = ?, position = ?, salary = ?, joining_date = ?, status = ? WHERE id = ?"); + $stmt->execute([$dept_id, $name, $email, $phone, $pos, $salary, $j_date, $status, $id]); + $message = "Employee updated successfully!"; + } + } + if (isset($_POST['delete_hr_employee'])) { + $id = (int)$_POST['id']; + if ($id) { + $stmt = db()->prepare("DELETE FROM hr_employees WHERE id = ?"); + $stmt->execute([$id]); + $message = "Employee deleted successfully!"; + } + } + if (isset($_POST['mark_attendance'])) { + $emp_id = (int)$_POST['employee_id']; + $date = $_POST['attendance_date'] ?: date('Y-m-d'); + $status = $_POST['status'] ?? 'present'; + $in = $_POST['clock_in'] ?: null; + $out = $_POST['clock_out'] ?: null; + if ($emp_id) { + $stmt = db()->prepare("INSERT INTO hr_attendance (employee_id, attendance_date, status, clock_in, clock_out) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE status = ?, clock_in = ?, clock_out = ?"); + $stmt->execute([$emp_id, $date, $status, $in, $out, $status, $in, $out]); + $message = "Attendance marked successfully!"; + } + } + if (isset($_POST['generate_payroll'])) { + $emp_id = (int)$_POST['employee_id']; + $month = (int)$_POST['month']; + $year = (int)$_POST['year']; + $bonus = (float)($_POST['bonus'] ?? 0); + $deductions = (float)($_POST['deductions'] ?? 0); + + $emp = db()->prepare("SELECT salary FROM hr_employees WHERE id = ?"); + $emp->execute([$emp_id]); + $salary = (float)$emp->fetchColumn(); + + $net = $salary + $bonus - $deductions; + + try { + $stmt = db()->prepare("INSERT INTO hr_payroll (employee_id, payroll_month, payroll_year, basic_salary, bonus, deductions, net_salary, status) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending')"); + $stmt->execute([$emp_id, $month, $year, $salary, $bonus, $deductions, $net]); + $message = "Payroll generated successfully!"; + } catch (PDOException $e) { + if ($e->getCode() == 23000) { // Integrity constraint violation + $message = "Error: Payroll already exists for this employee in the selected period."; + } else { + $message = "Error: " . $e->getMessage(); + } + } + } + if (isset($_POST['pay_payroll'])) { + $id = (int)$_POST['id']; + if ($id) { + $stmt = db()->prepare("UPDATE hr_payroll SET status = 'paid', payment_date = CURDATE() WHERE id = ?"); + $stmt->execute([$id]); + $message = "Payroll marked as paid!"; + } + } + if (isset($_POST['delete_payroll'])) { + $id = (int)$_POST['id']; + if ($id) { + $stmt = db()->prepare("DELETE FROM hr_payroll WHERE id = ?"); + $stmt->execute([$id]); + $message = "Payroll record deleted successfully!"; + } + } +} + // Routing & Data Fetching $page = $_GET['page'] ?? 'dashboard'; @@ -1567,6 +1687,30 @@ switch ($page) { $stmt->execute($params); $data['expiry_items'] = $stmt->fetchAll(); break; + case 'hr_departments': + $data['departments'] = db()->query("SELECT * FROM hr_departments ORDER BY id DESC")->fetchAll(); + break; + case 'hr_employees': + $data['employees'] = db()->query("SELECT e.*, d.name as dept_name FROM hr_employees e LEFT JOIN hr_departments d ON e.department_id = d.id ORDER BY e.id DESC")->fetchAll(); + $data['departments'] = db()->query("SELECT * FROM hr_departments ORDER BY name ASC")->fetchAll(); + break; + case 'hr_attendance': + $date = $_GET['date'] ?? date('Y-m-d'); + $data['attendance_date'] = $date; + $data['employees'] = db()->query("SELECT e.id, e.name, d.name as dept_name, a.status, a.clock_in, a.clock_out + FROM hr_employees e + LEFT JOIN hr_departments d ON e.department_id = d.id + LEFT JOIN hr_attendance a ON e.id = a.employee_id AND a.attendance_date = '$date' + WHERE e.status = 'active' ORDER BY e.name ASC")->fetchAll(); + break; + case 'hr_payroll': + $month = (int)($_GET['month'] ?? date('m')); + $year = (int)($_GET['year'] ?? date('Y')); + $data['month'] = $month; + $data['year'] = $year; + $data['payroll'] = db()->query("SELECT p.*, e.name as emp_name FROM hr_payroll p JOIN hr_employees e ON p.employee_id = e.id WHERE p.payroll_month = $month AND p.payroll_year = $year ORDER BY p.id DESC")->fetchAll(); + $data['employees'] = db()->query("SELECT id, name, salary FROM hr_employees WHERE status = 'active' ORDER BY name ASC")->fetchAll(); + break; default: $data['customers'] = db()->query("SELECT * FROM customers WHERE type = 'customer' ORDER BY id DESC LIMIT 5")->fetchAll(); // Dashboard stats @@ -1733,8 +1877,25 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; Company Profile - - HR + + + + +
+ + Departments + + + Employees + + + Attendance + + + Payroll
@@ -1761,6 +1922,10 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; 'supplier_statement' => ['en' => 'Supplier Statement', 'ar' => 'كشف حساب مورد'], 'expiry_report' => ['en' => 'Expiry Report', 'ar' => 'تقرير انتهاء الصلاحية'], 'settings' => ['en' => 'Company Profile', 'ar' => 'ملف الشركة'], + 'hr_departments' => ['en' => 'HR Departments', 'ar' => 'أقسام الموارد البشرية'], + 'hr_employees' => ['en' => 'HR Employees', 'ar' => 'موظفي الموارد البشرية'], + 'hr_attendance' => ['en' => 'HR Attendance', 'ar' => 'حضور الموارد البشرية'], + 'hr_payroll' => ['en' => 'HR Payroll', 'ar' => 'رواتب الموارد البشرية'], ]; $currTitle = $titles[$page] ?? $titles['dashboard']; ?> @@ -4064,6 +4229,374 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; + +
+
+
HR Departments
+ +
+
+ + + + + + + + + + + + + + + + + + + +
IDDepartment NameActions
+ +
+ + +
+
+
+
+ + +
+
+
HR Employees
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
NameDepartmentPositionSalaryStatusActions
+
+
+
OMR + + + + + +
+ + +
+
+
+
+ + +
+
+
HR Attendance
+
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
EmployeeDepartmentStatusClock InClock OutAction
+ + + + + + Not Marked + + + +
+
+
+ + +
+
+
HR Payroll
+
+
+ + + +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
EmployeeBasicBonusDeductionsNet SalaryStatusActions
OMR + OMR - OMR OMR + + + + + +
+ + +
+ +
+ + +
+
+
+
+ + + +
Company Profile
@@ -4128,6 +4661,85 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
+ + + + + +