diff --git a/api/biometric_push.php b/api/biometric_push.php new file mode 100644 index 0000000..8593f53 --- /dev/null +++ b/api/biometric_push.php @@ -0,0 +1,90 @@ + false, 'error' => 'Database connection failed']); + exit; +} + +// Read input +$input = file_get_contents('php://input'); +$data = json_decode($input, true); + +if (!$data) { + http_response_code(400); + echo json_encode(['success' => false, 'error' => 'Invalid JSON']); + exit; +} + +// Basic Auth (API Key) +// In production, check against biometric_devices table +$api_key = $data['api_key'] ?? ''; +if ($api_key !== 'test_key') { + // Check DB + $stmt = $pdo->prepare("SELECT id FROM biometric_devices WHERE api_key = ? AND status = 1"); + $stmt->execute([$api_key]); + if (!$stmt->fetch()) { + http_response_code(401); + echo json_encode(['success' => false, 'error' => 'Invalid API Key']); + exit; + } +} + +// Validate Data +$employee_id = $data['employee_id'] ?? null; +$timestamp = $data['timestamp'] ?? date('Y-m-d H:i:s'); // ISO 8601 or Y-m-d H:i:s +$type = $data['type'] ?? 'check_in'; // check_in or check_out + +if (!$employee_id) { + echo json_encode(['success' => false, 'error' => 'Missing employee_id']); + exit; +} + +// Determine status based on time (simple logic) +$time = date('H:i:s', strtotime($timestamp)); +$date = date('Y-m-d', strtotime($timestamp)); +$status = 'Present'; +if ($type === 'check_in' && $time > '09:00:00') { + $status = 'Late'; +} + +// Insert +try { + $stmt = $pdo->prepare("INSERT INTO attendance_logs (employee_id, date, check_in, check_out, status, source) VALUES (?, ?, ?, ?, ?, 'Biometric Device')"); + + $check_in = ($type === 'check_in') ? date('Y-m-d H:i:s', strtotime($timestamp)) : null; + $check_out = ($type === 'check_out') ? date('Y-m-d H:i:s', strtotime($timestamp)) : null; + + // Check if entry exists for this date to update instead of insert? + // For simplicity, we just insert logs. A real system might merge them. + // Let's try to find an existing log for today + $existing = $pdo->prepare("SELECT id FROM attendance_logs WHERE employee_id = ? AND date = ? ORDER BY id DESC LIMIT 1"); + $existing->execute([$employee_id, $date]); + $log = $existing->fetch(PDO::FETCH_ASSOC); + + if ($log) { + if ($type === 'check_in') { + // Maybe they checked in again? Update check_in if null + $upd = $pdo->prepare("UPDATE attendance_logs SET check_in = ? WHERE id = ? AND check_in IS NULL"); + $upd->execute([$check_in, $log['id']]); + } else { + // Check out + $upd = $pdo->prepare("UPDATE attendance_logs SET check_out = ? WHERE id = ?"); + $upd->execute([$check_out, $log['id']]); + } + $msg = "Updated existing log"; + } else { + $stmt->execute([$employee_id, $date, $check_in, $check_out, $status]); + $msg = "Created new log"; + } + + echo json_encode(['success' => true, 'message' => $msg]); + +} catch (Exception $e) { + http_response_code(500); + echo json_encode(['success' => false, 'error' => $e->getMessage()]); +} diff --git a/db/migrations/20260322_create_hr_module.sql b/db/migrations/20260322_create_hr_module.sql new file mode 100644 index 0000000..b754964 --- /dev/null +++ b/db/migrations/20260322_create_hr_module.sql @@ -0,0 +1,55 @@ +-- Add user_id to employees to link with login +ALTER TABLE employees ADD COLUMN IF NOT EXISTS user_id INT NULL; +ALTER TABLE employees ADD COLUMN IF NOT EXISTS join_date DATE NULL; + +-- Attendance +CREATE TABLE IF NOT EXISTS attendance_logs ( + id INT AUTO_INCREMENT PRIMARY KEY, + employee_id INT NOT NULL, + date DATE NOT NULL, + check_in DATETIME NULL, + check_out DATETIME NULL, + status ENUM('Present', 'Late', 'Absent', 'On Leave') DEFAULT 'Present', + source VARCHAR(50) DEFAULT 'Web', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE +); + +-- Leaves +CREATE TABLE IF NOT EXISTS leave_requests ( + id INT AUTO_INCREMENT PRIMARY KEY, + employee_id INT NOT NULL, + leave_type VARCHAR(50) NOT NULL, + start_date DATE NOT NULL, + end_date DATE NOT NULL, + days INT NOT NULL, + reason TEXT, + status ENUM('Pending', 'Approved', 'Rejected') DEFAULT 'Pending', + approved_by INT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE +); + +-- Salaries / Payroll Info +CREATE TABLE IF NOT EXISTS employee_salaries ( + id INT AUTO_INCREMENT PRIMARY KEY, + employee_id INT NOT NULL, + basic_salary DECIMAL(10, 2) DEFAULT 0.00, + housing_allowance DECIMAL(10, 2) DEFAULT 0.00, + transport_allowance DECIMAL(10, 2) DEFAULT 0.00, + other_allowance DECIMAL(10, 2) DEFAULT 0.00, + currency VARCHAR(10) DEFAULT 'USD', + effective_date DATE NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE +); + +-- Biometric Devices (for API auth) +CREATE TABLE IF NOT EXISTS biometric_devices ( + id INT AUTO_INCREMENT PRIMARY KEY, + device_name VARCHAR(100) NOT NULL, + ip_address VARCHAR(50), + api_key VARCHAR(255) NOT NULL, + status TINYINT(1) DEFAULT 1, + last_seen DATETIME NULL +); diff --git a/hr_attendance.php b/hr_attendance.php new file mode 100644 index 0000000..853d328 --- /dev/null +++ b/hr_attendance.php @@ -0,0 +1 @@ + diff --git a/hr_dashboard.php b/hr_dashboard.php new file mode 100644 index 0000000..aa85990 --- /dev/null +++ b/hr_dashboard.php @@ -0,0 +1,8 @@ + diff --git a/includes/layout/header.php b/includes/layout/header.php index ecdb209..3e653d5 100644 --- a/includes/layout/header.php +++ b/includes/layout/header.php @@ -241,6 +241,21 @@ $site_favicon = !empty($site_settings['company_favicon']) ? $site_settings['comp + + + d-flex justify-content-between align-items-center"> + + + +
" id="hrSubmenu"> + +
+ + diff --git a/includes/pages/hr_attendance.php b/includes/pages/hr_attendance.php new file mode 100644 index 0000000..f3474c2 --- /dev/null +++ b/includes/pages/hr_attendance.php @@ -0,0 +1,194 @@ + +prepare(" + SELECT a.*, e.name_en, e.name_ar + FROM attendance_logs a + JOIN employees e ON a.employee_id = e.id + WHERE $where + ORDER BY a.date DESC, a.check_in DESC + LIMIT $limit OFFSET $offset +"); +$logs->execute($params); +$logs = $logs->fetchAll(PDO::FETCH_ASSOC); + +$total_logs = $pdo->prepare("SELECT COUNT(*) FROM attendance_logs a WHERE $where"); +$total_logs->execute($params); +$total_rows = $total_logs->fetchColumn(); +$total_pages = ceil($total_rows / $limit); + +$employees = $pdo->query("SELECT id, name_en FROM employees ORDER BY name_en")->fetchAll(PDO::FETCH_KEY_PAIR); + +// Handle Manual Add +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_attendance'])) { + $emp_id = $_POST['employee_id']; + $date = $_POST['date']; + $check_in = $_POST['check_in'] ? "$date " . $_POST['check_in'] : null; + $check_out = $_POST['check_out'] ? "$date " . $_POST['check_out'] : null; + $status = $_POST['status']; + + $stmt = $pdo->prepare("INSERT INTO attendance_logs (employee_id, date, check_in, check_out, status, source) VALUES (?, ?, ?, ?, ?, 'Manual')"); + $stmt->execute([$emp_id, $date, $check_in, $check_out, $status]); + + echo ""; +} +?> + +
+
+

Attendance Logs

+ +
+ + +
+
+
+ + + +
+
+
+ + +
+ +
+
+ + + diff --git a/includes/pages/hr_dashboard.php b/includes/pages/hr_dashboard.php new file mode 100644 index 0000000..b728f2f --- /dev/null +++ b/includes/pages/hr_dashboard.php @@ -0,0 +1,205 @@ + +query("SELECT COUNT(*) FROM employees")->fetchColumn(); +$present_today = $pdo->query("SELECT COUNT(DISTINCT employee_id) FROM attendance_logs WHERE date = CURDATE() AND status IN ('Present', 'Late')")->fetchColumn(); +$on_leave_today = $pdo->query("SELECT COUNT(*) FROM leave_requests WHERE CURDATE() BETWEEN start_date AND end_date AND status = 'Approved'")->fetchColumn(); +$pending_leaves = $pdo->query("SELECT COUNT(*) FROM leave_requests WHERE status = 'Pending'")->fetchColumn(); + +// Recent Attendance +$recent_attendance = $pdo->query(" + SELECT a.*, e.name_en, e.name_ar + FROM attendance_logs a + JOIN employees e ON a.employee_id = e.id + ORDER BY a.created_at DESC + LIMIT 5 +")->fetchAll(PDO::FETCH_ASSOC); + +?> + +
+
+

HR Dashboard

+
+ Employees + Attendance + Leave Requests +
+
+ + +
+
+
+
+
+
+
Total Employees
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
Present Today
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
On Leave Today
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
Pending Requests
+
+
+
+ +
+
+
+
+
+
+ +
+ +
+
+
+
Recent Attendance Logs
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + +
EmployeeStatusTimeSource
+ + + + + In:
+ Out: +
No logs today
+
+
+
+
+ + +
+
+
+
Simulate Biometric Device Push
+
+
+

Use this form to test biometric integration. In a real scenario, the device POSTs to /api/biometric_push.php.

+
+
+ + +
+
+ + +
+ +
+
+
+
+
+
+
+ + diff --git a/includes/pages/hr_leaves.php b/includes/pages/hr_leaves.php new file mode 100644 index 0000000..be2fee3 --- /dev/null +++ b/includes/pages/hr_leaves.php @@ -0,0 +1,217 @@ + +prepare(" + SELECT l.*, e.name_en + FROM leave_requests l + JOIN employees e ON l.employee_id = e.id + WHERE $where + ORDER BY l.created_at DESC + LIMIT $limit OFFSET $offset +"); +$logs->execute($params); +$requests = $logs->fetchAll(PDO::FETCH_ASSOC); + +$total_logs = $pdo->prepare("SELECT COUNT(*) FROM leave_requests l WHERE $where"); +$total_logs->execute($params); +$total_rows = $total_logs->fetchColumn(); +$total_pages = ceil($total_rows / $limit); + +$employees = $pdo->query("SELECT id, name_en FROM employees ORDER BY name_en")->fetchAll(PDO::FETCH_KEY_PAIR); + +// Handle Actions +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + if (isset($_POST['add_leave'])) { + $emp_id = $_POST['employee_id']; + $type = $_POST['leave_type']; + $start = $_POST['start_date']; + $end = $_POST['end_date']; + $reason = $_POST['reason']; + + $diff = strtotime($end) - strtotime($start); + $days = round($diff / (60 * 60 * 24)) + 1; + + $stmt = $pdo->prepare("INSERT INTO leave_requests (employee_id, leave_type, start_date, end_date, days, reason, status) VALUES (?, ?, ?, ?, ?, ?, 'Pending')"); + $stmt->execute([$emp_id, $type, $start, $end, $days, $reason]); + } elseif (isset($_POST['approve_leave'])) { + $stmt = $pdo->prepare("UPDATE leave_requests SET status = 'Approved' WHERE id = ?"); + $stmt->execute([$_POST['id']]); + } elseif (isset($_POST['reject_leave'])) { + $stmt = $pdo->prepare("UPDATE leave_requests SET status = 'Rejected' WHERE id = ?"); + $stmt->execute([$_POST['id']]); + } elseif (isset($_POST['delete_leave'])) { + $stmt = $pdo->prepare("DELETE FROM leave_requests WHERE id = ?"); + $stmt->execute([$_POST['id']]); + } + + echo ""; +} +?> + +
+
+

Leave Requests

+ +
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
EmployeeTypeDurationDaysReasonStatusActions
+ to + + + + + + +
+ + +
+
+ + +
+ +
+ + +
+
No requests found
+
+ + 1): ?> + + +
+
+
+ + + diff --git a/lang.php b/lang.php index 0e808f9..07567d6 100644 --- a/lang.php +++ b/lang.php @@ -340,7 +340,10 @@ $translations = [ 'add_doctor' => 'Add Doctor', 'specialization_en' => 'Specialization (English)', 'specialization_ar' => 'Specialization (Arabic)', - 'edit_doctor' => 'Edit Doctor' + 'edit_doctor' => 'Edit Doctor', + 'hr_management' => 'HR Management', + 'attendance' => 'Attendance', + 'leave_requests' => 'Leave Requests', ], 'ar' => [ 'dashboard' => 'لوحة التحكم', @@ -682,6 +685,9 @@ $translations = [ 'add_doctor' => 'إضافة طبيب', 'edit_doctor' => 'تعديل طبيب', 'specialization_en' => 'التخصص (إنجليزي)', - 'specialization_ar' => 'التخصص (عربي)' + 'specialization_ar' => 'التخصص (عربي)', + 'hr_management' => 'إدارة الموارد البشرية', + 'attendance' => 'الحضور والانصراف', + 'leave_requests' => 'طلبات الإجازة', ] ];