|
Server : LiteSpeed System : Linux terra.hostitbro.com 5.14.0-611.54.3.el9_7.x86_64 #1 SMP PREEMPT_DYNAMIC Thu May 7 16:31:24 EDT 2026 x86_64 User : outerorb ( 1091) PHP Version : 8.1.34 Disable Function : mail Directory : /home2/outerorb/emp.outerorbittech.in/admin/ | |
|
Path: /home2/outerorb/emp.outerorbittech.in/admin/attendance.php
Size: 40.58 KB
Permissions: 0666
<?php
require __DIR__ . '/../includes/helpers.php';
require_admin();
ensure_attendance_tables();
ensure_employees_table();
ensure_holidays_table();
$roleLabel = 'Admin';
$pdo = db();
$flash = flash();
$admin = current_admin();
// Get filters
$year = isset($_GET['year']) ? (int) $_GET['year'] : (int) date('Y');
$month = isset($_GET['month']) ? (int) $_GET['month'] : (int) date('m');
$employeeId = isset($_GET['employee_id']) ? (int) $_GET['employee_id'] : 0;
$departmentFilter = isset($_GET['department']) ? sanitize_text($_GET['department']) : '';
$searchQuery = isset($_GET['q']) ? sanitize_text($_GET['q']) : '';
$fromDate = isset($_GET['from_date']) ? sanitize_text($_GET['from_date']) : '';
$toDate = isset($_GET['to_date']) ? sanitize_text($_GET['to_date']) : '';
// validate date range input; if invalid or empty, fallback to current month options
$isFromValid = DateTime::createFromFormat('Y-m-d', $fromDate) !== false;
$isToValid = DateTime::createFromFormat('Y-m-d', $toDate) !== false;
if ($isFromValid) { $fromDate = DateTime::createFromFormat('Y-m-d', $fromDate)->format('Y-m-d'); } else { $fromDate = ''; }
if ($isToValid) { $toDate = DateTime::createFromFormat('Y-m-d', $toDate)->format('Y-m-d'); } else { $toDate = ''; }
if ($fromDate && !$toDate) { $toDate = $fromDate; }
if ($toDate && !$fromDate) { $fromDate = $toDate; }
if ($fromDate && $toDate && $fromDate > $toDate) { $tmp=$fromDate; $fromDate=$toDate; $toDate=$tmp; }
if ($month < 1) { $month = 12; $year--; }
if ($month > 12) { $month = 1; $year++; }
// Validate month/year
if ($year < 2020 || $year > 2030) { $year = (int) date('Y'); }
if ($month < 1 || $month > 12) { $month = (int) date('m'); }
// If date range was not provided, default to selected month range.
if ($fromDate === '' || $toDate === '') {
$fromDate = date('Y-m-01', strtotime("{$year}-{$month}-01"));
$toDate = date('Y-m-t', strtotime("{$year}-{$month}-01"));
}
// Get employees for dropdown
$empWhere = ['e.deleted_at IS NULL', "e.employee_status = 'Active'"];
$empParams = [];
$allowedDepts = allowed_departments_for_admin();
if (!empty($allowedDepts)) {
$phs = [];
foreach ($allowedDepts as $i => $d) { $k = ':d'.$i; $phs[] = $k; $empParams[$k] = $d; }
$empWhere[] = 'e.department IN (' . implode(',', $phs) . ')';
}
if ($departmentFilter !== '') {
$empWhere[] = 'e.department = :dept';
$empParams[':dept'] = $departmentFilter;
}
$empSql = 'SELECT e.id, e.first_name, e.last_name, e.department, e.card_number FROM employees e WHERE ' . implode(' AND ', $empWhere) . ' ORDER BY e.first_name, e.last_name';
$empStmt = $pdo->prepare($empSql);
$empStmt->execute($empParams);
$allEmployees = $empStmt->fetchAll();
// Get selected employee details
$selectedEmployee = null;
if ($employeeId > 0) {
foreach ($allEmployees as $emp) {
if ($emp['id'] == $employeeId) {
$selectedEmployee = $emp;
break;
}
}
}
// Build attendance query by date range
$where = ['al.attend_date BETWEEN :from_date AND :to_date'];
$params = [':from_date' => $fromDate, ':to_date' => $toDate];
if ($employeeId > 0) {
$where[] = 'al.employee_id = :emp_id';
$params[':emp_id'] = $employeeId;
} elseif ($departmentFilter !== '') {
$where[] = 'e.department = :dept';
$params[':dept'] = $departmentFilter;
} elseif (!empty($allowedDepts)) {
$phs = [];
foreach ($allowedDepts as $i => $d) { $k = ':d'.$i; $phs[] = $k; $params[$k] = $d; }
$where[] = 'e.department IN (' . implode(',', $phs) . ')';
}
if ($searchQuery !== '') {
$where[] = '(CONCAT(e.first_name, " ", e.last_name) LIKE :q_name OR e.card_number LIKE :q_card OR CAST(e.id AS CHAR) LIKE :q_id)';
$params[':q_name'] = "%{$searchQuery}%";
$params[':q_card'] = "%{$searchQuery}%";
$params[':q_id'] = "%{$searchQuery}%";
}
$sql = 'SELECT al.*, e.first_name, e.last_name, e.card_number, e.department
FROM attendance_logs al
JOIN employees e ON e.id = al.employee_id
WHERE ' . implode(' AND ', $where) . '
ORDER BY e.first_name, e.last_name, al.attend_date, al.punch_in';
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$attendanceRecords = $stmt->fetchAll();
// Group by employee for display
$groupedRecords = [];
foreach ($attendanceRecords as $record) {
$empId = $record['employee_id'];
if (!isset($groupedRecords[$empId])) {
$groupedRecords[$empId] = [
'employee' => [
'id' => $record['employee_id'],
'name' => trim($record['first_name'] . ' ' . $record['last_name']),
'card_number' => $record['card_number'],
'department' => $record['department']
],
'records' => []
];
}
$groupedRecords[$empId]['records'][] = $record;
}
// Handle POST actions (import, save)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!verify_csrf($_POST['csrf_token'] ?? '')) {
redirect_with_message('attendance.php', 'Session expired.', 'error');
}
$action = $_POST['action'] ?? '';
// Save single attendance record
if ($action === 'save') {
$empId = (int) ($_POST['employee_id'] ?? 0);
$date = sanitize_text($_POST['attend_date'] ?? '');
$status = in_array($_POST['status'] ?? '', ['present','absent','half_day','wfh','holiday','on_leave'])
? $_POST['status'] : 'present';
$punchIn = sanitize_text($_POST['punch_in'] ?? '') ?: null;
$punchOut = sanitize_text($_POST['punch_out'] ?? '') ?: null;
$notes = sanitize_text($_POST['notes'] ?? '') ?: null;
if ($empId && $date) {
$shift = get_shift_for_department(null);
$isLate = 0;
if ($punchIn && $status === 'present') {
$graceSec = ($shift['grace_minutes'] ?? 15) * 60;
$shiftStart = strtotime(date('Y-m-d') . ' ' . $shift['start_time']);
$punchTs = strtotime(date('Y-m-d') . ' ' . $punchIn);
$isLate = ($punchTs > $shiftStart + $graceSec) ? 1 : 0;
}
$pdo->prepare(
'INSERT INTO attendance_logs (employee_id, attend_date, status, punch_in, punch_out, is_late, notes, marked_by)
VALUES (?,?,?,?,?,?,?,?)
ON DUPLICATE KEY UPDATE status=VALUES(status), punch_in=VALUES(punch_in),
punch_out=VALUES(punch_out), is_late=VALUES(is_late), notes=VALUES(notes),
marked_by=VALUES(marked_by), updated_at=NOW()'
)->execute([$empId, $date, $status, $punchIn, $punchOut, $isLate, $notes, $admin['id'] ?? null]);
}
redirect_with_message('attendance.php?' . http_build_query($_GET), 'Attendance saved.');
}
// Bulk import from CSV/XLS/XLSX
if ($action === 'import') {
$res = handle_upload('attendance_file', ['text/csv','application/vnd.ms-excel','application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'], 10 * 1024 * 1024);
if (!empty($res['error'])) {
redirect_with_message('attendance.php', 'Import failed: ' . $res['error'], 'error');
}
$path = $res['path'];
$imported = 0; $skipped = 0; $unmatched = 0;
$origName = $_FILES['attendance_file']['name'] ?? '';
$ext = strtolower(pathinfo($origName, PATHINFO_EXTENSION));
// XLS/XLSX processing
if (in_array($ext, ['xls','xlsx'], true)) {
$vendor = __DIR__ . '/../vendor/autoload.php';
if (!file_exists($vendor)) {
redirect_with_message('attendance.php', 'Please install PHPSpreadsheet: run `composer require phpoffice/phpspreadsheet` in the project root.', 'error');
}
require $vendor;
try {
$spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($path);
$sheet = $spreadsheet->getActiveSheet();
$rows = $sheet->toArray(null, true, true, true);
if (empty($rows) || count($rows) < 1) {
redirect_with_message('attendance.php', 'Import failed: spreadsheet appears empty.', 'error');
}
// Find header row
$rowsList = array_values($rows);
$header = null; $headerRowIndex = null;
$scanLimit = min(120, count($rowsList));
$normalize = function($cell){
$k = strtolower(trim(preg_replace('/[^a-z0-9_ ]+/', '', (string)$cell)));
return str_replace(' ', '_', $k);
};
for ($i = 0; $i < $scanLimit; $i++) {
$r = array_values($rowsList[$i]);
$norms = array_map($normalize, $r);
if (in_array('card_no', $norms, true) || in_array('card', $norms, true) || in_array('cardnumber', $norms, true)) {
$header = $r; $headerRowIndex = $i; break;
}
if ((in_array('s_no', $norms, true) || in_array('sno', $norms, true)) &&
(in_array('emp_code', $norms, true) || in_array('emp_name', $norms, true) || in_array('empname', $norms, true))) {
$header = $r; $headerRowIndex = $i; break;
}
}
if ($header === null) {
for ($i = 0; $i < $scanLimit; $i++) {
$r = array_values($rowsList[$i]);
foreach ($r as $cell) {
if ($cell === null) continue;
if (mb_stripos((string)$cell, 'card') !== false || mb_stripos((string)$cell, 'card no') !== false || mb_stripos((string)$cell, 'badge') !== false) {
$header = $r; $headerRowIndex = $i; break 2;
}
}
}
}
if ($header === null) { $header = array_values($rowsList[0]); $headerRowIndex = 0; }
$map = []; $rawHeader = [];
foreach ($header as $i => $h) { $map[$normalize($h)] = $i; $rawHeader[$i] = (string)$h; }
$cardIdx = null; $inIdx = null; $outIdx = null; $dateIdx = null;
foreach ($rawHeader as $i => $h) {
if ($cardIdx === null && mb_stripos($h, 'card') !== false) $cardIdx = $i;
if ($inIdx === null && (mb_stripos($h, 'in') !== false || mb_stripos($h, 'in time') !== false)) $inIdx = $i;
if ($outIdx === null && (mb_stripos($h, 'out') !== false || mb_stripos($h, 'out time') !== false)) $outIdx = $i;
if ($dateIdx === null && mb_stripos($h, 'date') !== false && mb_stripos($h, 'time') === false) $dateIdx = $i;
}
$cardCandidates = ['card_number','card_no','card','cardnumber','cardno','badge_number','badge_no','badge','employee_card','emp_card','card_id','uid','emp_code'];
if ($cardIdx === null) { foreach ($cardCandidates as $c) { if (isset($map[$c])) { $cardIdx = $map[$c]; break; } } }
$inCandidates = ['punch_in','time_in','in_time','in','check_in','clock_in','punch','time'];
if ($inIdx === null) { foreach ($inCandidates as $c) { if (isset($map[$c])) { $inIdx = $map[$c]; break; } } }
$outCandidates = ['punch_out','time_out','out_time','out','check_out','clock_out','punch'];
if ($outIdx === null) { foreach ($outCandidates as $c) { if (isset($map[$c])) { $outIdx = $map[$c]; break; } } }
if ($dateIdx === null) { foreach ($map as $k => $i) { if (strpos($k, 'date') !== false && strpos($k,'time') === false) { $dateIdx = $i; break; } } }
if ($cardIdx === null) {
$found = implode(', ', array_keys($map));
redirect_with_message('attendance.php', 'Import failed: could not find a Card Number column. Found headers: ' . $found, 'error');
}
$started = false; $rowIndex = 0;
foreach ($rows as $r) {
if ($rowIndex++ <= $headerRowIndex) { continue; }
$vals = array_values($r);
$cardRaw = $vals[$cardIdx] ?? '';
$card = trim((string)$cardRaw);
if ($card === '' && is_numeric($cardRaw)) { $card = (string)$cardRaw; }
if (strpos($card, '.') !== false) { $card = preg_replace('/\.0+$/', '', $card); }
$card = preg_replace('/[^0-9A-Za-z]/', '', $card);
if ($card === '') { $skipped++; continue; }
$rawIn = trim($vals[$inIdx] ?? '');
$rawOut = trim($vals[$outIdx] ?? '');
$rawDate = trim($vals[$dateIdx] ?? '');
$attDate = null; $punchIn = null; $punchOut = null;
if ($rawIn !== '') { $ts = strtotime($rawIn); if ($ts !== false) { $attDate = date('Y-m-d', $ts); $punchIn = date('H:i:s', $ts); } }
if ($rawOut !== '') { $ts = strtotime($rawOut); if ($ts !== false) { $attDate = $attDate ?: date('Y-m-d', $ts); $punchOut = date('H:i:s', $ts); } }
if ($attDate === null && $rawDate !== '') { $dts = strtotime($rawDate); if ($dts !== false) $attDate = date('Y-m-d', $dts); }
if ($attDate === null) { $skipped++; continue; }
$stmt = $pdo->prepare('SELECT id, department FROM employees WHERE card_number = ? LIMIT 1');
$stmt->execute([$card]);
$emp = $stmt->fetch();
if (!$emp && ctype_digit($card)) {
$stmt = $pdo->prepare('SELECT id, department FROM employees WHERE id = ? LIMIT 1');
$stmt->execute([(int)$card]);
$emp = $stmt->fetch();
}
if (!$emp) { $unmatched++; continue; }
$shift = get_shift_for_department($emp['department'] ?? null);
$isLate = 0;
if ($punchIn && isset($shift['start_time'])) {
$graceSec = ($shift['grace_minutes'] ?? 15) * 60;
$shiftStart = strtotime($attDate . ' ' . $shift['start_time']);
$pTs = strtotime($attDate . ' ' . $punchIn);
if ($pTs !== false && $pTs > $shiftStart + $graceSec) $isLate = 1;
}
$pdo->prepare(
'INSERT INTO attendance_logs (employee_id, attend_date, status, punch_in, punch_out, is_late, marked_by)
VALUES (?,?,?,?,?,?,?)
ON DUPLICATE KEY UPDATE status=VALUES(status), punch_in=COALESCE(VALUES(punch_in), punch_in),
punch_out=COALESCE(VALUES(punch_out), punch_out), is_late=VALUES(is_late), marked_by=VALUES(marked_by), updated_at=NOW()'
)->execute([$emp['id'], $attDate, 'present', $punchIn, $punchOut, $isLate, $admin['id'] ?? null]);
$imported++;
}
$msg = "Imported: $imported; Skipped: $skipped; Unmatched cards: $unmatched";
redirect_with_message('attendance.php?' . http_build_query($_GET), $msg);
} catch (Throwable $e) {
redirect_with_message('attendance.php', 'Import failed: ' . $e->getMessage(), 'error');
}
}
// CSV processing
if (($fh = @fopen($path, 'r')) !== false) {
$headers = fgetcsv($fh);
if ($headers === false) { fclose($fh); redirect_with_message('attendance.php', 'Import failed: empty file or not a CSV.', 'error'); }
$rowsPreview = [$headers];
$previewLimit = 80;
for ($i = 1; $i < $previewLimit; $i++) {
$peek = fgetcsv($fh);
if ($peek === false) break;
$rowsPreview[] = $peek;
}
$header = null; $headerIndex = 0;
$cardCandidates = ['card_number','card_no','card','cardnumber','cardno','badge_number','badge_no','badge','employee_card','emp_card','card_id','uid'];
foreach ($rowsPreview as $ri => $rrow) {
$norms = array_map($normalize ?? function($v) {
$k = strtolower(trim(preg_replace('/[^a-z0-9_ ]+/', '', (string)$v)));
return str_replace(' ', '_', $k);
}, $rrow);
if (in_array('card_no', $norms, true) || in_array('card', $norms, true) || in_array('cardnumber', $norms, true)) { $header = $rrow; $headerIndex = $ri; break; }
if ((in_array('s_no', $norms, true) || in_array('sno', $norms, true)) && (in_array('emp_code', $norms, true) || in_array('emp_name', $norms, true) || in_array('empname', $norms, true))) { $header = $rrow; $headerIndex = $ri; break; }
foreach ($cardCandidates as $c) { if (in_array($c, $norms, true)) { $header = $rrow; $headerIndex = $ri; break 2; } }
}
if ($header === null) { $header = $rowsPreview[0]; $headerIndex = 0; }
$map = [];
foreach ($header as $i => $h) { $map[$normalize($h)] = $i; }
$cardIdx = null;
foreach ($cardCandidates as $c) { if (isset($map[$c])) { $cardIdx = $map[$c]; break; } }
if ($cardIdx === null) {
foreach ($map as $k => $i) { if (strpos($k, 'card') !== false || strpos($k,'badge') !== false || (strpos($k,'id')!==false && strpos($k,'date')===false)) { $cardIdx = $i; break; } }
}
$inCandidates = ['punch_in','time_in','in_time','in','check_in','clock_in','punch','time'];
$inIdx = null; foreach ($inCandidates as $c) { if (isset($map[$c])) { $inIdx = $map[$c]; break; } }
$outCandidates = ['punch_out','time_out','out_time','out','check_out','clock_out','punch'];
$outIdx = null; foreach ($outCandidates as $c) { if (isset($map[$c])) { $outIdx = $map[$c]; break; } }
$dateIdx = null; foreach ($map as $k => $i) { if (strpos($k, 'date') !== false && strpos($k,'time') === false) { $dateIdx = $i; break; } }
if ($cardIdx === null) { fclose($fh); $found = implode(', ', array_keys($map)); redirect_with_message('attendance.php', 'Import failed: could not find a Card Number column. Found headers: ' . $found, 'error'); }
rewind($fh);
for ($s = 0; $s <= $headerIndex; $s++) { if (fgetcsv($fh) === false) break; }
while (($row = fgetcsv($fh)) !== false) {
$cardRaw = $row[$cardIdx] ?? '';
$card = trim((string)$cardRaw);
if ($card === '' && is_numeric($cardRaw)) { $card = (string)$cardRaw; }
if (strpos($card, '.') !== false) { $card = preg_replace('/\.0+$/', '', $card); }
$card = preg_replace('/[^0-9A-Za-z]/', '', $card);
if ($card === '') { $skipped++; continue; }
$rawIn = trim($row[$inIdx] ?? '');
$rawOut = trim($row[$outIdx] ?? '');
$rawDate = trim($row[$dateIdx] ?? '');
$attDate = null; $punchIn = null; $punchOut = null;
if ($rawIn !== '') { $ts = strtotime($rawIn); if ($ts !== false) { $attDate = date('Y-m-d', $ts); $punchIn = date('H:i:s', $ts); } }
if ($rawOut !== '') { $ts = strtotime($rawOut); if ($ts !== false) { $attDate = $attDate ?: date('Y-m-d', $ts); $punchOut = date('H:i:s', $ts); } }
if ($attDate === null && $rawDate !== '') { $dts = strtotime($rawDate); if ($dts !== false) $attDate = date('Y-m-d', $dts); }
if ($attDate === null) { $skipped++; continue; }
$stmt = $pdo->prepare('SELECT id, department FROM employees WHERE card_number = ? LIMIT 1');
$stmt->execute([$card]);
$emp = $stmt->fetch();
if (!$emp && ctype_digit($card)) {
$stmt = $pdo->prepare('SELECT id, department FROM employees WHERE id = ? LIMIT 1');
$stmt->execute([(int)$card]);
$emp = $stmt->fetch();
}
if (!$emp) { $unmatched++; continue; }
$shift = get_shift_for_department($emp['department'] ?? null);
$isLate = 0;
if ($punchIn && isset($shift['start_time'])) {
$graceSec = ($shift['grace_minutes'] ?? 15) * 60;
$shiftStart = strtotime($attDate . ' ' . $shift['start_time']);
$pTs = strtotime($attDate . ' ' . $punchIn);
if ($pTs !== false && $pTs > $shiftStart + $graceSec) $isLate = 1;
}
$pdo->prepare(
'INSERT INTO attendance_logs (employee_id, attend_date, status, punch_in, punch_out, is_late, marked_by)
VALUES (?,?,?,?,?,?,?)
ON DUPLICATE KEY UPDATE status=VALUES(status), punch_in=COALESCE(VALUES(punch_in), punch_in),
punch_out=COALESCE(VALUES(punch_out), punch_out), is_late=VALUES(is_late), marked_by=VALUES(marked_by), updated_at=NOW()'
)->execute([$emp['id'], $attDate, 'present', $punchIn, $punchOut, $isLate, $admin['id'] ?? null]);
$imported++;
}
fclose($fh);
$msg = "Imported: $imported; Skipped: $skipped; Unmatched cards: $unmatched";
redirect_with_message('attendance.php?' . http_build_query($_GET), $msg);
}
redirect_with_message('attendance.php', 'Import failed: could not read uploaded file.', 'error');
}
}
// Get departments for filter
$departments = list_departments();
// Helper function
function e(?string $v): string { return $v === null ? '' : htmlspecialchars($v, ENT_QUOTES, 'UTF-8'); }
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Attendance Management - <?php echo e($roleLabel); ?></title>
<link rel="stylesheet" href="../assets/css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
/* Override container centering for left alignment */
.container {
align-items: flex-start !important;
max-width: 1300px !important;
width: 95vw !important;
}
/* Make filters and import side by side */
.filters-and-import {
display: flex;
gap: 20px;
align-items: flex-start;
margin-bottom: 20px;
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
.filters-section {
flex: 1.1;
min-width: 250px;
max-width: 700px;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
padding: 18px;
border: 1px solid rgba(255, 255, 255, 0.1);
box-sizing: border-box;
}
.import-section {
flex: 0.9;
min-width: 220px;
max-width: 440px;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
padding: 20px;
border: 1px solid rgba(255, 255, 255, 0.1);
box-sizing: border-box;
}
.filters-section {
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.filters-row {
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}
.filter-group {
display: flex;
flex-direction: column;
min-width: 150px;
}
.filter-group label {
font-size: 14px;
font-weight: 500;
margin-bottom: 5px;
color: #bfdbfe;
}
.filter-group select,
.filter-group input {
padding: 8px 12px;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
background: rgba(255, 255, 255, 0.05);
color: #ffffff;
font-size: 14px;
}
.filter-group select:focus,
.filter-group input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
}
.btn-filter {
padding: 8px 16px;
background: #3b82f6;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
}
.btn-filter:hover {
background: #2563eb;
}
.attendance-table-container {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(10px);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
overflow: hidden;
margin-top: 20px;
}
.attendance-main-table {
width: 100%;
border-collapse: collapse;
}
.attendance-main-table thead {
background: rgba(255, 255, 255, 0.08);
}
.attendance-main-table th {
padding: 1rem;
text-align: left;
color: #cbd5e1;
font-size: 0.875rem;
font-weight: 600;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.attendance-main-table td {
padding: 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
color: #e2e8f0;
}
.attendance-main-table tbody tr:hover {
background: rgba(255, 255, 255, 0.03);
}
.employee-info {
display: flex;
flex-direction: column;
}
.employee-name {
font-weight: 600;
color: #e5e7eb;
}
.employee-card {
color: #bfdbfe;
font-size: 0.875rem;
}
.status-badge {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: 6px;
font-size: 0.75rem;
font-weight: 600;
}
.status-present {
background: rgba(16, 185, 129, 0.2);
color: #10b981;
}
.status-absent {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
}
.status-half-day {
background: rgba(245, 158, 11, 0.2);
color: #f59e0b;
}
.status-work-from-home {
background: rgba(37, 99, 235, 0.2);
color: #2563eb;
}
.status-holiday {
background: rgba(139, 92, 246, 0.2);
color: #8b5cf6;
}
.status-on-leave {
background: rgba(156, 163, 175, 0.2);
color: #9ca3af;
}
.time-display {
font-family: 'Courier New', monospace;
font-weight: 500;
color: #93c5fd;
}
.no-time {
color: #64748b;
}
.status-late {
color: #ef4444;
font-weight: 600;
}
.import-section {
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.import-form {
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}
.import-form input[type="file"] {
flex: 1;
min-width: 200px;
}
.btn-import {
padding: 8px 16px;
background: #10b981;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
}
.btn-import:hover {
background: #059669;
}
.month-nav {
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
margin-bottom: 20px;
}
.month-nav a {
color: #3b82f6;
text-decoration: none;
padding: 8px 16px;
border-radius: 6px;
transition: background-color 0.2s;
}
.month-nav a:hover {
background: rgba(59, 130, 246, 0.1);
}
.current-month {
font-size: 18px;
font-weight: 600;
color: #e5e7eb;
}
@media (max-width: 768px) {
.container {
width: 100% !important;
}
.filters-and-import {
flex-direction: column;
}
.filters-row {
flex-direction: column;
align-items: stretch;
}
.filter-group {
min-width: auto;
}
.import-form {
flex-direction: column;
align-items: stretch;
}
.attendance-main-table {
font-size: 0.875rem;
}
.attendance-main-table th,
.attendance-main-table td {
padding: 0.75rem;
}
.employee-info {
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.employee-card {
font-size: 0.75rem;
}
}
</style>
</head>
<body class="dashboard-layout">
<?php include __DIR__ . '/_sidebar.php'; ?>
<div class="main-content">
<div class="container">
<h1><i class="fas fa-calendar-check"></i> Attendance Management</h1>
<?php if ($flash): ?>
<div class="alert alert-<?php echo $flash['type']; ?>">
<?php echo e($flash['message']); ?>
</div>
<?php endif; ?>
<!-- Month Navigation -->
<div class="month-nav">
<?php
$prevMonth = $month - 1; $prevYear = $year;
if ($prevMonth < 1) { $prevMonth = 12; $prevYear--; }
$nextMonth = $month + 1; $nextYear = $year;
if ($nextMonth > 12) { $nextMonth = 1; $nextYear++; }
$queryParams = $_GET;
unset($queryParams['from_date'], $queryParams['to_date']);
?>
<a href="?<?php echo http_build_query(array_merge($queryParams, ['year' => $prevYear, 'month' => $prevMonth])); ?>">
<i class="fas fa-chevron-left"></i> Previous
</a>
<div class="current-month">
<?php echo date('F Y', mktime(0, 0, 0, $month, 1, $year)); ?>
</div>
<a href="?<?php echo http_build_query(array_merge($queryParams, ['year' => $nextYear, 'month' => $nextMonth])); ?>">
Next <i class="fas fa-chevron-right"></i>
</a>
</div>
<!-- Filters and Import Section -->
<div class="filters-and-import">
<div class="filters-section">
<form method="GET" class="filters-row">
<input type="hidden" name="year" value="<?php echo $year; ?>">
<input type="hidden" name="month" value="<?php echo $month; ?>">
<div class="filter-group">
<label for="employee_id">Employee:</label>
<select name="employee_id" id="employee_id">
<option value="">All Employees</option>
<?php foreach ($allEmployees as $emp): ?>
<option value="<?php echo $emp['id']; ?>" <?php echo $employeeId == $emp['id'] ? 'selected' : ''; ?>>
<?php echo e(trim($emp['first_name'] . ' ' . $emp['last_name'])); ?>
<?php if ($emp['card_number']): ?> (<?php echo e($emp['card_number']); ?>)<?php endif; ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="filter-group">
<label for="department">Department:</label>
<select name="department" id="department">
<option value="">All Departments</option>
<?php foreach ($departments as $dept): ?>
<option value="<?php echo e($dept); ?>" <?php echo $departmentFilter === $dept ? 'selected' : ''; ?>>
<?php echo e($dept); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="filter-group">
<label for="from_date">From Date:</label>
<input type="date" name="from_date" id="from_date" value="<?php echo e($fromDate); ?>">
</div>
<div class="filter-group">
<label for="to_date">To Date:</label>
<input type="date" name="to_date" id="to_date" value="<?php echo e($toDate); ?>">
</div>
<div class="filter-group">
<label for="q">Search:</label>
<input type="text" name="q" id="q" value="<?php echo e($searchQuery); ?>" placeholder="Name, ID, or Card #">
</div>
<div class="filter-group">
<label> </label>
<button type="submit" class="btn-filter">
<i class="fas fa-search"></i> Filter
</button>
</div>
</form>
</div>
<!-- Import Section -->
<div class="import-section">
<h3><i class="fas fa-upload"></i> Import Attendance Data</h3>
<form method="POST" enctype="multipart/form-data" class="import-form">
<input type="hidden" name="csrf_token" value="<?php echo generate_csrf_token(); ?>">
<input type="hidden" name="action" value="import">
<input type="file" name="attendance_file" accept=".csv,.xls,.xlsx" required>
<button type="submit" class="btn-import">
<i class="fas fa-upload"></i> Import
</button>
</form>
<p style="margin-top: 10px; font-size: 14px; color: #bfdbfe;">
Upload CSV, XLS, or XLSX file with columns: Card Number, Punch In, Punch Out, Date
</p>
</div>
</div>
<!-- Attendance Records -->
<?php if (empty($attendanceRecords)): ?>
<div class="no-records">
<i class="fas fa-calendar-times" style="font-size: 48px; margin-bottom: 20px; opacity: 0.5;"></i>
<p>No attendance records found for the selected filters.</p>
</div>
<?php else: ?>
<div class="attendance-table-container">
<table class="attendance-main-table">
<thead>
<tr>
<th>Employee</th>
<th>Department</th>
<th>Date</th>
<th>Status</th>
<th>Punch In</th>
<th>Punch Out</th>
<th>Late</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<?php foreach ($attendanceRecords as $record): ?>
<tr>
<td>
<div class="employee-info">
<div class="employee-name"><?php echo e($record['first_name'] . ' ' . $record['last_name']); ?></div>
<div class="employee-card"><?php if ($record['card_number']): ?>Card: <?php echo e($record['card_number']); ?><?php endif; ?></div>
</div>
</td>
<td><?php echo e($record['department']); ?></td>
<td><?php echo date('d M Y', strtotime($record['attend_date'])); ?></td>
<td>
<span class="status-badge status-<?php echo str_replace('_', '-', $record['status']); ?>">
<?php
$statusLabels = [
'present' => 'Present',
'absent' => 'Absent',
'half_day' => 'Half Day',
'wfh' => 'Work from Home',
'holiday' => 'Holiday',
'on_leave' => 'On Leave'
];
echo $statusLabels[$record['status']] ?? ucfirst(str_replace('_', ' ', $record['status']));
?>
</span>
</td>
<td>
<?php if ($record['punch_in']): ?>
<span class="time-display"><?php echo date('H:i', strtotime($record['punch_in'])); ?></span>
<?php else: ?>
<span class="no-time">—</span>
<?php endif; ?>
</td>
<td>
<?php if ($record['punch_out']): ?>
<span class="time-display"><?php echo date('H:i', strtotime($record['punch_out'])); ?></span>
<?php else: ?>
<span class="no-time">—</span>
<?php endif; ?>
</td>
<td>
<?php if ($record['is_late']): ?>
<span class="status-late">Yes</span>
<?php else: ?>
<span class="no-time">No</span>
<?php endif; ?>
</td>
<td><?php echo e($record['notes'] ?? ''); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
<script>
// Auto-submit form when filters change
document.getElementById('employee_id').addEventListener('change', function() {
this.form.submit();
});
document.getElementById('department').addEventListener('change', function() {
this.form.submit();
});
// Search on enter key
document.getElementById('q').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
this.form.submit();
}
});
</script>
</body>
</html>