add HR
This commit is contained in:
parent
10338c6bc6
commit
f62878214d
90
api/biometric_push.php
Normal file
90
api/biometric_push.php
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['success' => 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()]);
|
||||||
|
}
|
||||||
55
db/migrations/20260322_create_hr_module.sql
Normal file
55
db/migrations/20260322_create_hr_module.sql
Normal file
@ -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
|
||||||
|
);
|
||||||
1
hr_attendance.php
Normal file
1
hr_attendance.php
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?php require_once 'includes/auth.php'; require_once 'includes/header.php'; require_once 'includes/pages/hr_attendance.php'; require_once 'includes/footer.php'; ?>
|
||||||
8
hr_dashboard.php
Normal file
8
hr_dashboard.php
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'includes/auth.php';
|
||||||
|
require_once 'includes/header.php';
|
||||||
|
|
||||||
|
// Logic is in includes/pages/hr_dashboard.php
|
||||||
|
require_once 'includes/pages/hr_dashboard.php';
|
||||||
|
|
||||||
|
require_once 'includes/footer.php';
|
||||||
1
hr_leaves.php
Normal file
1
hr_leaves.php
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?php require_once 'includes/auth.php'; require_once 'includes/header.php'; require_once 'includes/pages/hr_leaves.php'; require_once 'includes/footer.php'; ?>
|
||||||
@ -241,6 +241,21 @@ $site_favicon = !empty($site_settings['company_favicon']) ? $site_settings['comp
|
|||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
|
||||||
|
<?php if (has_permission("users")): // Temporary check, should be hr ?>
|
||||||
|
<a href="#hrSubmenu" data-bs-toggle="collapse" class="sidebar-link <?php echo in_array($section, ["hr_dashboard", "hr_attendance", "hr_leaves"]) ? "active" : ""; ?> d-flex justify-content-between align-items-center">
|
||||||
|
<span><i class="bi bi-person-badge-fill me-2"></i> <?php echo __("hr_management"); ?></span>
|
||||||
|
<i class="bi bi-chevron-down small"></i>
|
||||||
|
</a>
|
||||||
|
<div class="collapse <?php echo in_array($section, ["hr_dashboard", "hr_attendance", "hr_leaves"]) ? "show" : ""; ?>" id="hrSubmenu">
|
||||||
|
<div class="sidebar-submenu">
|
||||||
|
<a href="hr_dashboard.php" class="sidebar-link py-2 <?php echo $section === "hr_dashboard" ? "active" : ""; ?>"><i class="bi bi-speedometer2 me-2"></i> <?php echo __("dashboard"); ?></a>
|
||||||
|
<a href="hr_attendance.php" class="sidebar-link py-2 <?php echo $section === "hr_attendance" ? "active" : ""; ?>"><i class="bi bi-clock-history me-2"></i> <?php echo __("attendance"); ?></a>
|
||||||
|
<a href="hr_leaves.php" class="sidebar-link py-2 <?php echo $section === "hr_leaves" ? "active" : ""; ?>"><i class="bi bi-calendar-range me-2"></i> <?php echo __("leave_requests"); ?></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (has_permission('settings')): ?>
|
<?php if (has_permission('settings')): ?>
|
||||||
<a href="#settingsSubmenu" data-bs-toggle="collapse" class="sidebar-link <?php echo in_array($section, ['employees', 'positions', 'company_profile', 'cities', 'services', 'departments', 'queue_ads']) ? 'active' : ''; ?> d-flex justify-content-between align-items-center">
|
<a href="#settingsSubmenu" data-bs-toggle="collapse" class="sidebar-link <?php echo in_array($section, ['employees', 'positions', 'company_profile', 'cities', 'services', 'departments', 'queue_ads']) ? 'active' : ''; ?> d-flex justify-content-between align-items-center">
|
||||||
<span><i class="bi bi-gear me-2"></i> <?php echo __('settings'); ?></span>
|
<span><i class="bi bi-gear me-2"></i> <?php echo __('settings'); ?></span>
|
||||||
|
|||||||
194
includes/pages/hr_attendance.php
Normal file
194
includes/pages/hr_attendance.php
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
<?php $pdo = $db; ?>
|
||||||
|
<?php
|
||||||
|
if (!is_admin()) {
|
||||||
|
// header('Location: index.php'); exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
|
||||||
|
$limit = 15;
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
|
||||||
|
$where = "1=1";
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if (!empty($_GET['employee_id'])) {
|
||||||
|
$where .= " AND a.employee_id = ?";
|
||||||
|
$params[] = $_GET['employee_id'];
|
||||||
|
}
|
||||||
|
if (!empty($_GET['date'])) {
|
||||||
|
$where .= " AND a.date = ?";
|
||||||
|
$params[] = $_GET['date'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$logs = $pdo->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 "<script>window.location.href='hr_attendance.php';</script>";
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1 class="h3 mb-0 text-gray-800">Attendance Logs</h1>
|
||||||
|
<button class="btn btn-primary btn-sm" data-toggle="modal" data-target="#addAttendanceModal">
|
||||||
|
<i class="fas fa-plus"></i> Manual Entry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter -->
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="GET" class="form-inline">
|
||||||
|
<select name="employee_id" class="form-control mr-2 mb-2">
|
||||||
|
<option value="">All Employees</option>
|
||||||
|
<?php foreach ($employees as $id => $name): ?>
|
||||||
|
<option value="<?php echo $id; ?>" <?php echo (isset($_GET['employee_id']) && $_GET['employee_id'] == $id) ? 'selected' : ''; ?>>
|
||||||
|
<?php echo htmlspecialchars($name); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<input type="date" name="date" class="form-control mr-2 mb-2" value="<?php echo $_GET['date'] ?? ''; ?>">
|
||||||
|
<button type="submit" class="btn btn-primary mb-2">Filter</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Table -->
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered" width="100%" cellspacing="0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Employee</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Check In</th>
|
||||||
|
<th>Check Out</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Source</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($logs as $log): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?php echo $log['id']; ?></td>
|
||||||
|
<td><?php echo htmlspecialchars($log['name_en']); ?></td>
|
||||||
|
<td><?php echo $log['date']; ?></td>
|
||||||
|
<td><?php echo $log['check_in'] ? date('H:i', strtotime($log['check_in'])) : '-'; ?></td>
|
||||||
|
<td><?php echo $log['check_out'] ? date('H:i', strtotime($log['check_out'])) : '-'; ?></td>
|
||||||
|
<td>
|
||||||
|
<span class="badge badge-<?php
|
||||||
|
echo $log['status'] == 'Present' ? 'success' :
|
||||||
|
($log['status'] == 'Late' ? 'warning' : 'danger');
|
||||||
|
?>">
|
||||||
|
<?php echo $log['status']; ?>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td><?php echo htmlspecialchars($log['source']); ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php if (empty($logs)): ?>
|
||||||
|
<tr><td colspan="7" class="text-center">No logs found</td></tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
<?php if ($total_pages > 1): ?>
|
||||||
|
<nav aria-label="Page navigation">
|
||||||
|
<ul class="pagination justify-content-center">
|
||||||
|
<?php for ($i = 1; $i <= $total_pages; $i++): ?>
|
||||||
|
<li class="page-item <?php echo $i == $page ? 'active' : ''; ?>">
|
||||||
|
<a class="page-link" href="?page=<?php echo $i; ?>&employee_id=<?php echo $_GET['employee_id'] ?? ''; ?>&date=<?php echo $_GET['date'] ?? ''; ?>">
|
||||||
|
<?php echo $i; ?>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Modal -->
|
||||||
|
<div class="modal fade" id="addAttendanceModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<form method="POST">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Add Attendance Log</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" name="add_attendance" value="1">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Employee</label>
|
||||||
|
<select name="employee_id" class="form-control" required>
|
||||||
|
<?php foreach ($employees as $id => $name): ?>
|
||||||
|
<option value="<?php echo $id; ?>"><?php echo htmlspecialchars($name); ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Date</label>
|
||||||
|
<input type="date" name="date" class="form-control" required value="<?php echo date('Y-m-d'); ?>">
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="col-6">
|
||||||
|
<label>Check In Time</label>
|
||||||
|
<input type="time" name="check_in" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<label>Check Out Time</label>
|
||||||
|
<input type="time" name="check_out" class="form-control">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Status</label>
|
||||||
|
<select name="status" class="form-control">
|
||||||
|
<option value="Present">Present</option>
|
||||||
|
<option value="Late">Late</option>
|
||||||
|
<option value="Absent">Absent</option>
|
||||||
|
<option value="On Leave">On Leave</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||||
|
<button type="submit" class="btn btn-primary">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
205
includes/pages/hr_dashboard.php
Normal file
205
includes/pages/hr_dashboard.php
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
<?php $pdo = $db; ?>
|
||||||
|
<?php
|
||||||
|
if (!is_admin()) {
|
||||||
|
// Only admins for now, or roles with permission
|
||||||
|
// header('Location: index.php'); exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch Stats
|
||||||
|
$total_employees = $pdo->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);
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1 class="h3 mb-0 text-gray-800">HR Dashboard</h1>
|
||||||
|
<div>
|
||||||
|
<a href="employees.php" class="btn btn-sm btn-primary shadow-sm"><i class="fas fa-users fa-sm text-white-50"></i> Employees</a>
|
||||||
|
<a href="hr_attendance.php" class="btn btn-sm btn-info shadow-sm"><i class="fas fa-clock fa-sm text-white-50"></i> Attendance</a>
|
||||||
|
<a href="hr_leaves.php" class="btn btn-sm btn-warning shadow-sm"><i class="fas fa-calendar-minus fa-sm text-white-50"></i> Leave Requests</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats Rows -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xl-3 col-md-6 mb-4">
|
||||||
|
<div class="card border-left-primary shadow h-100 py-2">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row no-gutters align-items-center">
|
||||||
|
<div class="col mr-2">
|
||||||
|
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">Total Employees</div>
|
||||||
|
<div class="h5 mb-0 font-weight-bold text-gray-800"><?php echo $total_employees; ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<i class="fas fa-users fa-2x text-gray-300"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-xl-3 col-md-6 mb-4">
|
||||||
|
<div class="card border-left-success shadow h-100 py-2">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row no-gutters align-items-center">
|
||||||
|
<div class="col mr-2">
|
||||||
|
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">Present Today</div>
|
||||||
|
<div class="h5 mb-0 font-weight-bold text-gray-800"><?php echo $present_today; ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<i class="fas fa-user-check fa-2x text-gray-300"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-xl-3 col-md-6 mb-4">
|
||||||
|
<div class="card border-left-info shadow h-100 py-2">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row no-gutters align-items-center">
|
||||||
|
<div class="col mr-2">
|
||||||
|
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">On Leave Today</div>
|
||||||
|
<div class="h5 mb-0 font-weight-bold text-gray-800"><?php echo $on_leave_today; ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<i class="fas fa-plane-departure fa-2x text-gray-300"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-xl-3 col-md-6 mb-4">
|
||||||
|
<div class="card border-left-warning shadow h-100 py-2">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row no-gutters align-items-center">
|
||||||
|
<div class="col mr-2">
|
||||||
|
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">Pending Requests</div>
|
||||||
|
<div class="h5 mb-0 font-weight-bold text-gray-800"><?php echo $pending_leaves; ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<i class="fas fa-clipboard-list fa-2x text-gray-300"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<!-- Recent Attendance -->
|
||||||
|
<div class="col-lg-6 mb-4">
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-header py-3">
|
||||||
|
<h6 class="m-0 font-weight-bold text-primary">Recent Attendance Logs</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered" width="100%" cellspacing="0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Employee</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Time</th>
|
||||||
|
<th>Source</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($recent_attendance as $log): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?php echo htmlspecialchars($log['name_en']); ?></td>
|
||||||
|
<td>
|
||||||
|
<span class="badge badge-<?php
|
||||||
|
echo $log['status'] == 'Present' ? 'success' :
|
||||||
|
($log['status'] == 'Late' ? 'warning' : 'danger');
|
||||||
|
?>">
|
||||||
|
<?php echo $log['status']; ?>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
In: <?php echo $log['check_in'] ? date('H:i', strtotime($log['check_in'])) : '-'; ?><br>
|
||||||
|
Out: <?php echo $log['check_out'] ? date('H:i', strtotime($log['check_out'])) : '-'; ?>
|
||||||
|
</td>
|
||||||
|
<td><?php echo htmlspecialchars($log['source']); ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php if(empty($recent_attendance)): ?>
|
||||||
|
<tr><td colspan="4" class="text-center">No logs today</td></tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Biometric Simulator -->
|
||||||
|
<div class="col-lg-6 mb-4">
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-header py-3">
|
||||||
|
<h6 class="m-0 font-weight-bold text-info">Simulate Biometric Device Push</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>Use this form to test biometric integration. In a real scenario, the device POSTs to <code>/api/biometric_push.php</code>.</p>
|
||||||
|
<form id="biometricSimForm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Employee ID</label>
|
||||||
|
<input type="number" class="form-control" name="employee_id" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Type</label>
|
||||||
|
<select class="form-control" name="type">
|
||||||
|
<option value="check_in">Check In</option>
|
||||||
|
<option value="check_out">Check Out</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary btn-block">Simulate Push</button>
|
||||||
|
</form>
|
||||||
|
<div id="simResult" class="mt-3"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('biometricSimForm').addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const formData = new FormData(this);
|
||||||
|
const data = {
|
||||||
|
api_key: 'test_key', // You would configure this per device
|
||||||
|
employee_id: formData.get('employee_id'),
|
||||||
|
type: formData.get('type'),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch('api/biometric_push.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(d => {
|
||||||
|
const resDiv = document.getElementById('simResult');
|
||||||
|
if(d.success) {
|
||||||
|
resDiv.innerHTML = '<div class="alert alert-success">Success: ' + d.message + '</div>';
|
||||||
|
setTimeout(() => location.reload(), 1500);
|
||||||
|
} else {
|
||||||
|
resDiv.innerHTML = '<div class="alert alert-danger">Error: ' + (d.error || 'Unknown') + '</div>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
217
includes/pages/hr_leaves.php
Normal file
217
includes/pages/hr_leaves.php
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
<?php $pdo = $db; ?>
|
||||||
|
<?php
|
||||||
|
if (!is_admin()) {
|
||||||
|
// header('Location: index.php'); exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
|
||||||
|
$limit = 15;
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
|
||||||
|
$where = "1=1";
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if (!empty($_GET['status'])) {
|
||||||
|
$where .= " AND l.status = ?";
|
||||||
|
$params[] = $_GET['status'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$logs = $pdo->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 "<script>window.location.href='hr_leaves.php';</script>";
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1 class="h3 mb-0 text-gray-800">Leave Requests</h1>
|
||||||
|
<button class="btn btn-primary btn-sm" data-toggle="modal" data-target="#addLeaveModal">
|
||||||
|
<i class="fas fa-plus"></i> New Request
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter -->
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="GET" class="form-inline">
|
||||||
|
<select name="status" class="form-control mr-2 mb-2">
|
||||||
|
<option value="">All Statuses</option>
|
||||||
|
<option value="Pending" <?php echo (isset($_GET['status']) && $_GET['status'] == 'Pending') ? 'selected' : ''; ?>>Pending</option>
|
||||||
|
<option value="Approved" <?php echo (isset($_GET['status']) && $_GET['status'] == 'Approved') ? 'selected' : ''; ?>>Approved</option>
|
||||||
|
<option value="Rejected" <?php echo (isset($_GET['status']) && $_GET['status'] == 'Rejected') ? 'selected' : ''; ?>>Rejected</option>
|
||||||
|
</select>
|
||||||
|
<button type="submit" class="btn btn-primary mb-2">Filter</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Table -->
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered" width="100%" cellspacing="0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Employee</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Duration</th>
|
||||||
|
<th>Days</th>
|
||||||
|
<th>Reason</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($requests as $req): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?php echo htmlspecialchars($req['name_en']); ?></td>
|
||||||
|
<td><?php echo htmlspecialchars($req['leave_type']); ?></td>
|
||||||
|
<td>
|
||||||
|
<?php echo $req['start_date']; ?> to <?php echo $req['end_date']; ?>
|
||||||
|
</td>
|
||||||
|
<td><?php echo $req['days']; ?></td>
|
||||||
|
<td><?php echo htmlspecialchars($req['reason']); ?></td>
|
||||||
|
<td>
|
||||||
|
<span class="badge badge-<?php
|
||||||
|
echo $req['status'] == 'Approved' ? 'success' :
|
||||||
|
($req['status'] == 'Pending' ? 'warning' : 'danger');
|
||||||
|
?>">
|
||||||
|
<?php echo $req['status']; ?>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?php if($req['status'] == 'Pending'): ?>
|
||||||
|
<form method="POST" style="display:inline;">
|
||||||
|
<input type="hidden" name="id" value="<?php echo $req['id']; ?>">
|
||||||
|
<button type="submit" name="approve_leave" class="btn btn-success btn-sm" title="Approve"><i class="fas fa-check"></i></button>
|
||||||
|
</form>
|
||||||
|
<form method="POST" style="display:inline;">
|
||||||
|
<input type="hidden" name="id" value="<?php echo $req['id']; ?>">
|
||||||
|
<button type="submit" name="reject_leave" class="btn btn-danger btn-sm" title="Reject"><i class="fas fa-times"></i></button>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
<form method="POST" style="display:inline;" onsubmit="return confirm('Delete this request?');">
|
||||||
|
<input type="hidden" name="id" value="<?php echo $req['id']; ?>">
|
||||||
|
<button type="submit" name="delete_leave" class="btn btn-secondary btn-sm" title="Delete"><i class="fas fa-trash"></i></button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php if (empty($requests)): ?>
|
||||||
|
<tr><td colspan="7" class="text-center">No requests found</td></tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!-- Pagination -->
|
||||||
|
<?php if ($total_pages > 1): ?>
|
||||||
|
<nav aria-label="Page navigation">
|
||||||
|
<ul class="pagination justify-content-center">
|
||||||
|
<?php for ($i = 1; $i <= $total_pages; $i++): ?>
|
||||||
|
<li class="page-item <?php echo $i == $page ? 'active' : ''; ?>">
|
||||||
|
<a class="page-link" href="?page=<?php echo $i; ?>&status=<?php echo $_GET['status'] ?? ''; ?>">
|
||||||
|
<?php echo $i; ?>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Modal -->
|
||||||
|
<div class="modal fade" id="addLeaveModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<form method="POST">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Request Leave</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" name="add_leave" value="1">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Employee</label>
|
||||||
|
<select name="employee_id" class="form-control" required>
|
||||||
|
<?php foreach ($employees as $id => $name): ?>
|
||||||
|
<option value="<?php echo $id; ?>"><?php echo htmlspecialchars($name); ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Leave Type</label>
|
||||||
|
<select name="leave_type" class="form-control">
|
||||||
|
<option value="Annual">Annual</option>
|
||||||
|
<option value="Sick">Sick</option>
|
||||||
|
<option value="Casual">Casual</option>
|
||||||
|
<option value="Unpaid">Unpaid</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="col-6">
|
||||||
|
<label>Start Date</label>
|
||||||
|
<input type="date" name="start_date" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<label>End Date</label>
|
||||||
|
<input type="date" name="end_date" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Reason</label>
|
||||||
|
<textarea name="reason" class="form-control" rows="2"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||||
|
<button type="submit" class="btn btn-primary">Submit</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
10
lang.php
10
lang.php
@ -340,7 +340,10 @@ $translations = [
|
|||||||
'add_doctor' => 'Add Doctor',
|
'add_doctor' => 'Add Doctor',
|
||||||
'specialization_en' => 'Specialization (English)',
|
'specialization_en' => 'Specialization (English)',
|
||||||
'specialization_ar' => 'Specialization (Arabic)',
|
'specialization_ar' => 'Specialization (Arabic)',
|
||||||
'edit_doctor' => 'Edit Doctor'
|
'edit_doctor' => 'Edit Doctor',
|
||||||
|
'hr_management' => 'HR Management',
|
||||||
|
'attendance' => 'Attendance',
|
||||||
|
'leave_requests' => 'Leave Requests',
|
||||||
],
|
],
|
||||||
'ar' => [
|
'ar' => [
|
||||||
'dashboard' => 'لوحة التحكم',
|
'dashboard' => 'لوحة التحكم',
|
||||||
@ -682,6 +685,9 @@ $translations = [
|
|||||||
'add_doctor' => 'إضافة طبيب',
|
'add_doctor' => 'إضافة طبيب',
|
||||||
'edit_doctor' => 'تعديل طبيب',
|
'edit_doctor' => 'تعديل طبيب',
|
||||||
'specialization_en' => 'التخصص (إنجليزي)',
|
'specialization_en' => 'التخصص (إنجليزي)',
|
||||||
'specialization_ar' => 'التخصص (عربي)'
|
'specialization_ar' => 'التخصص (عربي)',
|
||||||
|
'hr_management' => 'إدارة الموارد البشرية',
|
||||||
|
'attendance' => 'الحضور والانصراف',
|
||||||
|
'leave_requests' => 'طلبات الإجازة',
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user