<?php
// app/Controllers/HrController.php — v2: Smart Attendance + Staff Add from HR Panel

class HrController {

    // ─── Auth & Context ───────────────────────────────────────────────────────

    private function requireHr() {
        if (!isset($_SESSION['user_id'])) {
            header('Location: /login'); exit;
        }
        if (!in_array($_SESSION['role'], ['admin', 'hr_manager', 'cashier'])) {
            header('Location: /login'); exit;
        }
        FeatureGate::enforceStaffManagement($_SESSION['restaurant_id']);
    }

    private function getContext() {
        $role    = $_SESSION['role'];
        $rest_id = $_SESSION['restaurant_id'];

        if ($role === 'hr_manager') {
            $db   = getDB();
            $stmt = $db->prepare('SELECT cashier_id FROM users WHERE id = ?');
            $stmt->execute([$_SESSION['user_id']]);
            $u = $stmt->fetch();
            return [
                'rest_id'    => $rest_id,
                'cashier_id' => $u['cashier_id'] ?? null,
                'is_admin'   => false,
            ];
        }

        if ($role === 'cashier') {
            return [
                'rest_id'    => $rest_id,
                'cashier_id' => $_SESSION['user_id'],
                'is_admin'   => false,
            ];
        }

        $cashier_id = null;
        if (!empty($_GET['cashier_id'])) {
            $cashier_id = (int)$_GET['cashier_id'];
            $_SESSION['hr_cashier_id'] = $cashier_id;
        } elseif (!empty($_POST['cashier_id'])) {
            $cashier_id = (int)$_POST['cashier_id'];
            $_SESSION['hr_cashier_id'] = $cashier_id;
        } elseif (isset($_SESSION['hr_cashier_id'])) {
            $cashier_id = (int)$_SESSION['hr_cashier_id'];
        }
        return [
            'rest_id'    => $rest_id,
            'cashier_id' => $cashier_id ?: null,
            'is_admin'   => true,
        ];
    }

    private function baseUrl() {
        return $_SESSION['role'] === 'hr_manager' ? '/hr' : '/admin/hr';
    }

    private function redirect($type, $msg, $tab = '') {
        $base = $this->baseUrl();
        $qs   = $tab ? "?tab={$tab}" : '';
        header("Location: {$base}{$qs}&{$type}=" . urlencode($msg));
        exit;
    }

    private function tabRedirect($tab, $extra = '') {
        $base = $this->baseUrl();
        $url  = $tab === 'staff' ? $base : "{$base}/{$tab}";
        header("Location: {$url}?{$extra}");
        exit;
    }

    // ─── Staff query ──────────────────────────────────────────────────────────

    private function getStaff($ctx) {
        $db = getDB();
        if ($ctx['cashier_id']) {
            $stmt = $db->prepare("
                SELECT u.id, u.name, u.email, u.role, u.status,
                       sp.designation, sp.department, sp.join_date,
                       sp.salary_type, sp.base_salary, sp.overtime_rate,
                       sp.working_days_per_month, sp.phone, sp.address,
                       sp.emergency_contact, sp.shift_id, sp.clock_in_pin, sp.id AS profile_id,
                       ss.name AS shift_name, ss.start_time, ss.end_time
                FROM users u
                LEFT JOIN staff_profiles sp ON sp.user_id = u.id
                LEFT JOIN shift_schedules ss ON ss.id = sp.shift_id
                WHERE u.restaurant_id = ?
                  AND u.role IN ('cashier','waiter','kitchen','inventory','hr_manager')
                  AND u.status = 'active'
                  AND (u.cashier_id = ? OR u.id = ?)
                ORDER BY u.name ASC
            ");
            $stmt->execute([$ctx['rest_id'], $ctx['cashier_id'], $ctx['cashier_id']]);
        } else {
            $stmt = $db->prepare("
                SELECT u.id, u.name, u.email, u.role, u.status,
                       sp.designation, sp.department, sp.join_date,
                       sp.salary_type, sp.base_salary, sp.overtime_rate,
                       sp.working_days_per_month, sp.phone, sp.address,
                       sp.emergency_contact, sp.shift_id, sp.clock_in_pin, sp.id AS profile_id,
                       ss.name AS shift_name, ss.start_time, ss.end_time
                FROM users u
                LEFT JOIN staff_profiles sp ON sp.user_id = u.id
                LEFT JOIN shift_schedules ss ON ss.id = sp.shift_id
                WHERE u.restaurant_id = ?
                  AND u.role IN ('cashier','waiter','kitchen','inventory','hr_manager')
                  AND u.status = 'active'
                ORDER BY u.name ASC
            ");
            $stmt->execute([$ctx['rest_id']]);
        }
        return $stmt->fetchAll();
    }

    private function getShifts($ctx) {
        $db = getDB();
        $stmt = $db->prepare('SELECT * FROM shift_schedules WHERE restaurant_id = ? ORDER BY start_time ASC');
        $stmt->execute([$ctx['rest_id']]);
        return $stmt->fetchAll();
    }

    private function getCashiers($rest_id) {
        $db   = getDB();
        $stmt = $db->prepare('SELECT id, name, email FROM users WHERE restaurant_id = ? AND role = "cashier" AND status = "active" ORDER BY name ASC');
        $stmt->execute([$rest_id]);
        return $stmt->fetchAll();
    }

    // ─── INDEX — Staff Tab ────────────────────────────────────────────────────

    public function index() {
        $this->requireHr();
        FeatureGate::enforceSubscription($_SESSION['restaurant_id']);

        $db  = getDB();
        $ctx = $this->getContext();

        $cashiers = $this->getCashiers($ctx['rest_id']);
        $cashierName = null;
        if ($ctx['cashier_id']) {
            $cn = $db->prepare('SELECT name FROM users WHERE id = ?');
            $cn->execute([$ctx['cashier_id']]);
            $cashierName = $cn->fetchColumn();
        }

        $staff     = $this->getStaff($ctx);
        $shifts    = $this->getShifts($ctx);
        $activeTab = 'staff';
        $pageTitle = 'HR Management';

        ob_start();
        require 'app/Views/hr/index.php';
        $content = ob_get_clean();
        $this->render($content, $pageTitle);
    }

    // ─── ADD STAFF directly from HR panel ────────────────────────────────────

    public function addStaff() {
        $this->requireHr();
        $db  = getDB();
        $ctx = $this->getContext();

        $name      = trim($_POST['name'] ?? '');
        $role_post = $_POST['role'] ?? 'others';
        $allowedRoles = ['others', 'waiter'];

        if (!$name || !in_array($role_post, $allowedRoles)) {
            $this->redirect('error', 'All fields are required.', 'staff');
        }

        if ($role_post === 'waiter') {
            $checkLimit = FeatureGate::canAddWaiter($ctx['rest_id']);
            if (!$checkLimit['allowed']) {
                $this->redirect('error', 'Waiter limit reached. Upgrade your package to add more waiters.', 'staff');
            }
        } else {
            // Check limits for others staff
            $checkLimit = FeatureGate::canAddOther($ctx['rest_id']);
            if (!$checkLimit['allowed']) {
                $this->redirect('error', 'Other staff limit reached. Upgrade your package to add more staff.', 'staff');
            }
        }

        $email       = trim($_POST['email'] ?? '');
        $password    = $_POST['password'] ?? '';
        $designation = trim($_POST['designation'] ?? '') ?: null;

        if ($role_post === 'others') {
            $role = 'waiter';
            $designation = trim($_POST['custom_role'] ?? '') ?: 'Staff';
            // Auto generate email and password
            $email = 'staff_' . bin2hex(random_bytes(4)) . '_' . time() . '@restpo.local';
            $password = bin2hex(random_bytes(16));
        } else {
            $role = $role_post;
            if (!$email || !$password) {
                $this->redirect('error', 'All fields are required.', 'staff');
            }
        }

        $cashier_id_assign = !empty($_POST['assign_cashier_id']) ? (int)$_POST['assign_cashier_id'] : $ctx['cashier_id'];

        // Salary basics
        $salary_type = $_POST['salary_type'] ?? 'monthly';
        $base_salary = (float)($_POST['base_salary'] ?? 0);
        $department  = trim($_POST['department']  ?? '') ?: null;

        // Check email uniqueness
        $check = $db->prepare('SELECT id FROM users WHERE email = ?');
        $check->execute([$email]);
        if ($check->fetch()) {
            $this->redirect('error', 'Email already in use.', 'staff');
        }

        $hashed = password_hash($password, PASSWORD_BCRYPT);
        $db->prepare('INSERT INTO users (restaurant_id, cashier_id, name, email, password, role, status) VALUES (?,?,?,?,?,?,?)')
           ->execute([$ctx['rest_id'], $cashier_id_assign, $name, $email, $hashed, $role, 'active']);
        $newUserId = $db->lastInsertId();

        // Create basic profile
        $db->prepare('INSERT INTO staff_profiles (user_id, restaurant_id, cashier_id, designation, department, salary_type, base_salary) VALUES (?,?,?,?,?,?,?)')
           ->execute([$newUserId, $ctx['rest_id'], $cashier_id_assign, $designation, $department, $salary_type, $base_salary]);

        $this->redirect('success', "Staff member '{$name}' added successfully.", 'staff');
    }

    // ─── STAFF PROFILE UPDATE ─────────────────────────────────────────────────

    public function staffStore() {
        $this->requireHr();
        $db  = getDB();
        $ctx = $this->getContext();

        $user_id     = (int)($_POST['user_id'] ?? 0);
        $designation = trim($_POST['designation'] ?? '') ?: null;
        $department  = trim($_POST['department']  ?? '') ?: null;
        $join_date   = $_POST['join_date'] ?? null;
        $salary_type = $_POST['salary_type'] ?? 'monthly';
        $base_salary = (float)($_POST['base_salary'] ?? 0);
        $ot_rate     = (float)($_POST['overtime_rate'] ?? 0);
        $work_days   = (int)($_POST['working_days_per_month'] ?? 26);
        $shift_id    = !empty($_POST['shift_id']) ? (int)$_POST['shift_id'] : null;
        $phone       = trim($_POST['phone']    ?? '') ?: null;
        $address     = trim($_POST['address']  ?? '') ?: null;
        $emergency   = trim($_POST['emergency_contact'] ?? '') ?: null;
        $clock_pin   = trim($_POST['clock_in_pin'] ?? '') ?: null;

        if (!$user_id) { $this->redirect('error', 'Invalid staff member.', 'staff'); }

        $check = $db->prepare('SELECT id FROM users WHERE id = ? AND restaurant_id = ?');
        $check->execute([$user_id, $ctx['rest_id']]);
        if (!$check->fetch()) { $this->redirect('error', 'Staff member not found.', 'staff'); }

        if ($clock_pin !== null) {
            // Verify uniqueness within this restaurant
            $pinCheck = $db->prepare('
                SELECT sp.user_id 
                FROM staff_profiles sp 
                JOIN users u ON u.id = sp.user_id 
                WHERE sp.clock_in_pin = ? AND sp.restaurant_id = ? AND sp.user_id != ?
            ');
            $pinCheck->execute([$clock_pin, $ctx['rest_id'], $user_id]);
            if ($pinCheck->fetch()) {
                $this->redirect('error', 'This Clock-In PIN is already assigned to another employee.', 'staff');
            }
        }

        $existing = $db->prepare('SELECT id FROM staff_profiles WHERE user_id = ?');
        $existing->execute([$user_id]);
        if ($existing->fetch()) {
            $db->prepare('UPDATE staff_profiles SET designation=?, department=?, join_date=?, salary_type=?, base_salary=?, overtime_rate=?, working_days_per_month=?, shift_id=?, phone=?, address=?, emergency_contact=?, clock_in_pin=? WHERE user_id=?')
               ->execute([$designation, $department, $join_date ?: null, $salary_type, $base_salary, $ot_rate, $work_days, $shift_id, $phone, $address, $emergency, $clock_pin, $user_id]);
        } else {
            $db->prepare('INSERT INTO staff_profiles (user_id, restaurant_id, cashier_id, designation, department, join_date, salary_type, base_salary, overtime_rate, working_days_per_month, shift_id, phone, address, emergency_contact, clock_in_pin) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)')
               ->execute([$user_id, $ctx['rest_id'], $ctx['cashier_id'], $designation, $department, $join_date ?: null, $salary_type, $base_salary, $ot_rate, $work_days, $shift_id, $phone, $address, $emergency, $clock_pin]);
        }

        $this->syncDraftPayroll($db, $user_id, $ctx['rest_id'], $salary_type, $base_salary, $ot_rate);

        $this->redirect('success', 'Staff profile saved. Salary updated on draft payroll records.', 'staff');
    }

    // Auto-sync: when salary changes, update DRAFT payroll records (preserves gross/net recalc)
    private function syncDraftPayroll($db, $user_id, $rest_id, $salary_type, $base_salary, $ot_rate) {
        // Find all DRAFT records for this user
        $drafts = $db->prepare('SELECT * FROM payroll_records WHERE user_id = ? AND restaurant_id = ? AND status = "draft"');
        $drafts->execute([$user_id, $rest_id]);
        foreach ($drafts->fetchAll() as $pr) {
            // Recalculate gross with new salary, keeping existing attendance counts
            $workDays = $pr['working_days'] ?: 26;
            $dPresent = $pr['days_present'];
            $dAbsent  = $pr['days_absent'];
            $dLate    = $pr['days_late'];
            $dHalf    = $pr['days_half'] ?? 0;
            $otHours  = $pr['overtime_hours'];
            $otPay    = round($otHours * $ot_rate, 2);

            if ($salary_type === 'monthly') {
                $dailyRate = $workDays > 0 ? ($base_salary / $workDays) : 0;
                $gross = $base_salary
                       - ($dAbsent * $dailyRate)
                       - ($dLate   * $dailyRate * 0.25)
                       - ($dHalf   * $dailyRate * 0.5)
                       + $otPay;
            } elseif ($salary_type === 'daily') {
                $gross = ($dPresent * $base_salary)
                       + ($dHalf   * $base_salary * 0.5)
                       + ($dLate   * $base_salary * 0.75)
                       + $otPay;
            } else {
                // hourly: we don't have total_hours in payroll_records easily — keep gross as-is, just update rates
                $gross = $pr['gross_pay'];
            }
            $gross  = max(0, round($gross, 2));
            $net    = max(0, round($gross + $pr['bonus'] - $pr['deductions'], 2));

            $db->prepare('UPDATE payroll_records SET salary_type=?, base_salary=?, overtime_rate=?, overtime_pay=?, gross_pay=?, net_pay=? WHERE id=?')
               ->execute([$salary_type, $base_salary, $ot_rate, $otPay, $gross, $net, $pr['id']]);
        }
    }

    // ─── SHIFT SCHEDULES CRUD ─────────────────────────────────────────────────

    public function storeShift() {
        $this->requireHr();
        $db  = getDB();
        $ctx = $this->getContext();

        $id         = (int)($_POST['shift_id'] ?? 0);
        $name       = trim($_POST['shift_name'] ?? '');
        $start      = $_POST['start_time'] ?? '';
        $end        = $_POST['end_time']   ?? '';
        $late_min   = (int)($_POST['late_threshold_minutes']      ?? 15);
        $early_min  = (int)($_POST['early_out_threshold_minutes'] ?? 15);
        $is_default = !empty($_POST['is_default']) ? 1 : 0;

        if (!$name || !$start || !$end) {
            $this->redirect('error', 'Shift name, start time, and end time are required.', 'staff');
        }

        if ($is_default) {
            $db->prepare('UPDATE shift_schedules SET is_default = 0 WHERE restaurant_id = ?')->execute([$ctx['rest_id']]);
        }

        if ($id) {
            $db->prepare('UPDATE shift_schedules SET name=?, start_time=?, end_time=?, late_threshold_minutes=?, early_out_threshold_minutes=?, is_default=? WHERE id=? AND restaurant_id=?')
               ->execute([$name, $start, $end, $late_min, $early_min, $is_default, $id, $ctx['rest_id']]);
        } else {
            $db->prepare('INSERT INTO shift_schedules (restaurant_id, cashier_id, name, start_time, end_time, late_threshold_minutes, early_out_threshold_minutes, is_default) VALUES (?,?,?,?,?,?,?,?)')
               ->execute([$ctx['rest_id'], $ctx['cashier_id'], $name, $start, $end, $late_min, $early_min, $is_default]);
        }

        $this->redirect('success', 'Shift schedule saved.', 'staff');
    }

    public function deleteShift() {
        $this->requireHr();
        $db  = getDB();
        $ctx = $this->getContext();
        $id  = (int)($_POST['shift_id'] ?? 0);

        $db->prepare('DELETE FROM shift_schedules WHERE id = ? AND restaurant_id = ?')->execute([$id, $ctx['rest_id']]);
        $this->redirect('success', 'Shift deleted.', 'staff');
    }

    // ─── ATTENDANCE VIEW ──────────────────────────────────────────────────────

    public function attendance() {
        $this->requireHr();
        FeatureGate::enforceSubscription($_SESSION['restaurant_id']);

        $db  = getDB();
        $ctx = $this->getContext();

        $date = $_GET['date'] ?? date('Y-m-d');
        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) $date = date('Y-m-d');

        $cashiers = $this->getCashiers($ctx['rest_id']);
        $cashierName = null;
        if ($ctx['cashier_id']) {
            $cn = $db->prepare('SELECT name FROM users WHERE id = ?');
            $cn->execute([$ctx['cashier_id']]);
            $cashierName = $cn->fetchColumn();
        }

        // Fetch cashier locations
        $locs = $db->prepare('SELECT * FROM cashier_locations WHERE restaurant_id = ?');
        $locs->execute([$ctx['rest_id']]);
        $cashierLocations = [];
        foreach ($locs->fetchAll() as $row) {
            $cashierLocations[$row['cashier_id']] = $row;
        }

        $staff  = $this->getStaff($ctx);
        $shifts = $this->getShifts($ctx);

        // Load attendance for selected date
        $attendanceMap = [];
        if (!empty($staff)) {
            $ids  = array_column($staff, 'id');
            $ph   = implode(',', array_fill(0, count($ids), '?'));
            $aStmt = $db->prepare("SELECT * FROM attendance_logs WHERE user_id IN ($ph) AND log_date = ?");
            $aStmt->execute(array_merge($ids, [$date]));
            foreach ($aStmt->fetchAll() as $row) {
                $attendanceMap[$row['user_id']] = $row;
            }
        }

        // Monthly summary
        $month = date('Y-m', strtotime($date));
        $summaryMap = [];
        if (!empty($staff)) {
            $ids   = array_column($staff, 'id');
            $ph    = implode(',', array_fill(0, count($ids), '?'));
            $sStmt = $db->prepare("
                SELECT user_id,
                       SUM(status IN ('present','late','early_out')) AS present,
                       SUM(status IN ('absent', 'unpaid_leave'))   AS absent,
                       SUM(status = 'late')     AS late,
                       SUM(status = 'half_day') AS half_day,
                       ROUND(SUM(hours_worked),1) AS total_hours
                FROM attendance_logs
                WHERE user_id IN ($ph) AND log_date LIKE ?
                GROUP BY user_id
            ");
            $sStmt->execute(array_merge($ids, [$month . '%']));
            foreach ($sStmt->fetchAll() as $row) {
                $summaryMap[$row['user_id']] = $row;
            }
        }

        // Today's live status (who is currently checked in)
        $todayMap = [];
        if (!empty($staff) && $date === date('Y-m-d')) {
            $ids   = array_column($staff, 'id');
            $ph    = implode(',', array_fill(0, count($ids), '?'));
            $tStmt = $db->prepare("SELECT user_id, check_in, check_out, status FROM attendance_logs WHERE user_id IN ($ph) AND log_date = CURDATE()");
            $tStmt->execute($ids);
            foreach ($tStmt->fetchAll() as $row) {
                $todayMap[$row['user_id']] = $row;
            }
        }

        $activeTab = 'attendance';
        $pageTitle = 'HR — Attendance';
        ob_start();
        require 'app/Views/hr/index.php';
        $content = ob_get_clean();
        $this->render($content, $pageTitle);
    }

    // ─── MARK ATTENDANCE (Smart) ──────────────────────────────────────────────

    public function markAttendance() {
        $this->requireHr();
        $db  = getDB();
        $ctx = $this->getContext();

        $date    = $_POST['date'] ?? date('Y-m-d');
        $entries = $_POST['attendance'] ?? [];

        foreach ($entries as $uid => $row) {
            $uid      = (int)$uid;
            $checkIn  = !empty($row['check_in'])  ? $row['check_in']  : null;
            $checkOut = !empty($row['check_out']) ? $row['check_out'] : null;
            $statusOverride = $row['status'] ?? null;
            $ot       = (float)($row['overtime_hours'] ?? 0);
            $notes    = trim($row['notes'] ?? '') ?: null;

            // Verify ownership
            $check = $db->prepare('SELECT id FROM users WHERE id = ? AND restaurant_id = ?');
            $check->execute([$uid, $ctx['rest_id']]);
            if (!$check->fetch()) continue;

            // Get staff shift for smart detection
            $spStmt = $db->prepare('SELECT sp.shift_id, ss.start_time, ss.end_time, ss.late_threshold_minutes, ss.early_out_threshold_minutes FROM staff_profiles sp LEFT JOIN shift_schedules ss ON ss.id = sp.shift_id WHERE sp.user_id = ?');
            $spStmt->execute([$uid]);
            $sp = $spStmt->fetch();

            // ── Smart status detection ──
            $status       = $statusOverride ?: 'absent';
            $hoursWorked  = 0;
            $earlyOut     = 0;

            if ($checkIn) {
                $status = 'present'; // default to present if checked in

                if ($sp && $sp['start_time']) {
                    $shiftStart = strtotime($date . ' ' . $sp['start_time']);
                    $checkInTs  = strtotime($date . ' ' . $checkIn);
                    $lateBuffer = ($sp['late_threshold_minutes'] ?? 15) * 60;

                    if ($checkInTs > $shiftStart + $lateBuffer) {
                        $status = 'late';
                    }
                }

                if ($checkOut) {
                    $checkInTs  = strtotime($date . ' ' . $checkIn);
                    $checkOutTs = strtotime($date . ' ' . $checkOut);
                    if ($checkOutTs > $checkInTs) {
                        $hoursWorked = round(($checkOutTs - $checkInTs) / 3600, 2);
                    }

                    if ($sp && $sp['end_time']) {
                        $shiftEnd      = strtotime($date . ' ' . $sp['end_time']);
                        $earlyBuffer   = ($sp['early_out_threshold_minutes'] ?? 15) * 60;
                        if ($checkOutTs < $shiftEnd - $earlyBuffer) {
                            $earlyOut = 1;
                            // Only override to half_day if hours < 50% of shift
                            if ($sp['start_time']) {
                                $shiftHours = ($shiftEnd - strtotime($date . ' ' . $sp['start_time'])) / 3600;
                                if ($hoursWorked < ($shiftHours * 0.5)) {
                                    $status = 'half_day';
                                }
                            }
                        }
                    }
                }
            }

            // Manual override always wins if explicitly set and not 'auto'
            if ($statusOverride && $statusOverride !== 'auto') {
                $status = $statusOverride;
            }

            $allowed = ['present','absent','late','half_day','holiday','early_out','leave','unpaid_leave'];
            if (!in_array($status, $allowed)) $status = 'absent';

            $db->prepare("
                INSERT INTO attendance_logs (user_id, restaurant_id, cashier_id, log_date, check_in, check_out, status, overtime_hours, hours_worked, early_out, notes, marked_by)
                VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
                ON DUPLICATE KEY UPDATE
                    check_in=VALUES(check_in), check_out=VALUES(check_out),
                    status=VALUES(status), overtime_hours=VALUES(overtime_hours),
                    hours_worked=VALUES(hours_worked), early_out=VALUES(early_out),
                    notes=VALUES(notes), marked_by=VALUES(marked_by)
            ")->execute([$uid, $ctx['rest_id'], $ctx['cashier_id'], $date, $checkIn, $checkOut, $status, $ot, $hoursWorked, $earlyOut, $notes, $_SESSION['user_id']]);
        }

        $cqs = $ctx['cashier_id'] ? 'cashier_id=' . $ctx['cashier_id'] . '&' : '';
        header("Location: " . $this->baseUrl() . "/attendance?{$cqs}date=" . urlencode($date) . "&success=" . urlencode('Attendance saved. Status auto-detected from shift.'));
        exit;
    }

    // ─── PAYROLL VIEW ─────────────────────────────────────────────────────────

    public function payroll() {
        $this->requireHr();
        FeatureGate::enforceSubscription($_SESSION['restaurant_id']);

        $db  = getDB();
        $ctx = $this->getContext();

        $month = (int)($_GET['month'] ?? date('n'));
        $year  = (int)($_GET['year']  ?? date('Y'));
        $month = max(1, min(12, $month));
        $year  = max(2020, min(2100, $year));

        $cashiers    = $ctx['is_admin'] ? $this->getCashiers($ctx['rest_id']) : [];
        $cashierName = null;
        if ($ctx['cashier_id']) {
            $cn = $db->prepare('SELECT name FROM users WHERE id = ?');
            $cn->execute([$ctx['cashier_id']]);
            $cashierName = $cn->fetchColumn();
        }

        $staff = $this->getStaff($ctx);
        $payrollMap = [];
        if (!empty($staff)) {
            $ids = array_column($staff, 'id');
            $ph  = implode(',', array_fill(0, count($ids), '?'));
            $pStmt = $db->prepare("SELECT * FROM payroll_records WHERE user_id IN ($ph) AND period_month=? AND period_year=?");
            $pStmt->execute(array_merge($ids, [$month, $year]));
            foreach ($pStmt->fetchAll() as $row) $payrollMap[$row['user_id']] = $row;
        }

        $currency  = FeatureGate::currency($ctx['rest_id']);
        $activeTab = 'payroll';
        $pageTitle = 'HR — Payroll';
        ob_start();
        require 'app/Views/hr/index.php';
        $content = ob_get_clean();
        $this->render($content, $pageTitle);
    }

    // ─── GENERATE PAYROLL ─────────────────────────────────────────────────────

    public function generatePayroll() {
        $this->requireHr();
        $db  = getDB();
        $ctx = $this->getContext();

        $month = (int)($_POST['month'] ?? date('n'));
        $year  = (int)($_POST['year']  ?? date('Y'));

        $single_user_id = isset($_POST['user_id']) ? (int)$_POST['user_id'] : null;
        if ($single_user_id) {
            $stmt = $db->prepare("
                SELECT u.id, u.name, u.email, u.role, u.status,
                       sp.designation, sp.department, sp.join_date,
                       sp.salary_type, sp.base_salary, sp.overtime_rate,
                       sp.working_days_per_month, sp.phone, sp.address,
                       sp.emergency_contact, sp.shift_id, sp.id AS profile_id,
                       ss.name AS shift_name, ss.start_time, ss.end_time
                FROM users u
                LEFT JOIN staff_profiles sp ON sp.user_id = u.id
                LEFT JOIN shift_schedules ss ON ss.id = sp.shift_id
                WHERE u.restaurant_id = ? AND u.id = ?
            ");
            $stmt->execute([$ctx['rest_id'], $single_user_id]);
            $staff = $stmt->fetchAll();
        } else {
            $staff = $this->getStaff($ctx);
        }

        $generated = 0;

        foreach ($staff as $m) {
            $uid        = $m['id'];

            // Check if there is an existing record and if it's paid
            $existCheck = $db->prepare("SELECT status FROM payroll_records WHERE user_id = ? AND period_month = ? AND period_year = ? AND restaurant_id = ?");
            $existCheck->execute([$uid, $month, $year, $ctx['rest_id']]);
            $existingRecord = $existCheck->fetch();
            if ($existingRecord && $existingRecord['status'] === 'paid') {
                if (!$single_user_id) {
                    continue;
                }
            }

            $salaryType = $m['salary_type']            ?? 'monthly';
            $baseSalary = (float)($m['base_salary']    ?? 0);
            $otRate     = (float)($m['overtime_rate']  ?? 0);
            $workDays   = (int)($m['working_days_per_month'] ?? 26);

            $dateFrom = sprintf('%04d-%02d-01', $year, $month);
            $dateTo   = date('Y-m-t', strtotime($dateFrom));

            $aStmt = $db->prepare("
                SELECT
                    SUM(status IN ('present','late','early_out')) AS present,
                    SUM(status IN ('absent', 'unpaid_leave'))   AS absent,
                    SUM(status = 'late')     AS late,
                    SUM(status = 'half_day') AS half_day,
                    COALESCE(SUM(overtime_hours), 0) AS total_ot,
                    COALESCE(SUM(hours_worked), 0)   AS total_hours
                FROM attendance_logs
                WHERE user_id = ? AND log_date BETWEEN ? AND ?
            ");
            $aStmt->execute([$uid, $dateFrom, $dateTo]);
            $att = $aStmt->fetch();

            $dPresent  = (int)($att['present']    ?? 0);
            $dAbsent   = (int)($att['absent']     ?? 0);
            $dLate     = (int)($att['late']       ?? 0);
            $dHalf     = (int)($att['half_day']   ?? 0);
            $otHours   = (float)($att['total_ot'] ?? 0);
            $totalHrs  = (float)($att['total_hours'] ?? 0);
            $otPay     = round($otHours * $otRate, 2);

            if ($salaryType === 'monthly') {
                $dailyRate = $workDays > 0 ? ($baseSalary / $workDays) : 0;
                $gross = $baseSalary
                       - ($dAbsent * $dailyRate)
                       - ($dLate   * $dailyRate * 0.25)   // late = 25% daily deduction
                       - ($dHalf   * $dailyRate * 0.5)
                       + $otPay;
            } elseif ($salaryType === 'daily') {
                $gross = ($dPresent * $baseSalary)
                       + ($dHalf   * $baseSalary * 0.5)
                       + ($dLate   * $baseSalary * 0.75)  // late = 75% of day rate
                       + $otPay;
            } else {
                $gross = $totalHrs * $baseSalary + $otPay;
            }
            $gross = max(0, round($gross, 2));

            $db->prepare("
                INSERT INTO payroll_records
                    (user_id, restaurant_id, cashier_id, period_month, period_year,
                     salary_type, base_salary, working_days, days_present, days_absent,
                     days_late, days_half, overtime_hours, overtime_rate, overtime_pay,
                     bonus, deductions, gross_pay, net_pay, status, generated_by)
                VALUES (?,?,?,?,?, ?,?,?,?,?, ?,?,?,?,?, ?,?,?,?,'draft',?)
                ON DUPLICATE KEY UPDATE
                    salary_type=VALUES(salary_type), base_salary=VALUES(base_salary),
                    working_days=VALUES(working_days), days_present=VALUES(days_present),
                    days_absent=VALUES(days_absent), days_late=VALUES(days_late),
                    days_half=VALUES(days_half), overtime_hours=VALUES(overtime_hours),
                    overtime_rate=VALUES(overtime_rate), overtime_pay=VALUES(overtime_pay),
                    gross_pay=VALUES(gross_pay), net_pay=VALUES(net_pay),
                    generated_by=VALUES(generated_by)
            ")->execute([
                $uid, $ctx['rest_id'], $ctx['cashier_id'], $month, $year,
                $salaryType, $baseSalary, $workDays, $dPresent, $dAbsent,
                $dLate, $dHalf, $otHours, $otRate, $otPay,
                0, 0, $gross, $gross, $_SESSION['user_id']
            ]);
            $generated++;
        }

        $cqs = $ctx['cashier_id'] ? 'cashier_id=' . $ctx['cashier_id'] . '&' : '';
        $msg = $single_user_id ? "Payroll record refreshed." : "Payroll generated for {$generated} staff.";
        header("Location: " . $this->baseUrl() . "/payroll?{$cqs}month={$month}&year={$year}&success=" . urlencode($msg));
        exit;
    }

    // ─── UPDATE PAYROLL (bonus/deductions) ───────────────────────────────────

    public function updatePayroll() {
        $this->requireHr();
        $db  = getDB();
        $ctx = $this->getContext();

        $record_id  = (int)($_POST['record_id'] ?? 0);
        $bonus      = (float)($_POST['bonus']      ?? 0);
        $deductions = (float)($_POST['deductions'] ?? 0);
        $notes      = trim($_POST['notes'] ?? '') ?: null;
        $month      = (int)($_POST['month'] ?? date('n'));
        $year       = (int)($_POST['year']  ?? date('Y'));

        if (!$record_id) {
            $cqs = $ctx['cashier_id'] ? 'cashier_id=' . $ctx['cashier_id'] . '&' : '';
            header("Location: " . $this->baseUrl() . "/payroll?{$cqs}month={$month}&year={$year}&error=" . urlencode('Invalid record.'));
            exit;
        }

        $rec = $db->prepare('SELECT * FROM payroll_records WHERE id = ? AND restaurant_id = ? AND status = "draft"');
        $rec->execute([$record_id, $ctx['rest_id']]);
        $record = $rec->fetch();

        if (!$record) {
            $cqs = $ctx['cashier_id'] ? 'cashier_id=' . $ctx['cashier_id'] . '&' : '';
            header("Location: " . $this->baseUrl() . "/payroll?{$cqs}month={$month}&year={$year}&error=" . urlencode('Record not found or already paid.'));
            exit;
        }

        $net = max(0, (float)$record['gross_pay'] + $bonus - $deductions);
        $db->prepare('UPDATE payroll_records SET bonus=?, deductions=?, net_pay=?, notes=? WHERE id=?')
           ->execute([$bonus, $deductions, $net, $notes, $record_id]);

        $cqs = $ctx['cashier_id'] ? 'cashier_id=' . $ctx['cashier_id'] . '&' : '';
        header("Location: " . $this->baseUrl() . "/payroll?{$cqs}month={$month}&year={$year}&success=" . urlencode('Payroll updated.'));
        exit;
    }

    // ─── MARK PAID ────────────────────────────────────────────────────────────

    public function markPaid() {
        $this->requireHr();
        $db  = getDB();
        $ctx = $this->getContext();

        $record_id = (int)($_POST['record_id'] ?? 0);
        $month     = (int)($_POST['month'] ?? date('n'));
        $year      = (int)($_POST['year']  ?? date('Y'));

        $db->prepare('UPDATE payroll_records SET status="paid", paid_at=NOW() WHERE id=? AND restaurant_id=?')
           ->execute([$record_id, $ctx['rest_id']]);

        $cqs = $ctx['cashier_id'] ? 'cashier_id=' . $ctx['cashier_id'] . '&' : '';
        header("Location: " . $this->baseUrl() . "/payroll?{$cqs}month={$month}&year={$year}&success=" . urlencode('Marked as paid.'));
        exit;
    }

    // ─── DELETE PAYROLL ───────────────────────────────────────────────────────

    public function deletePayroll() {
        $this->requireHr();
        $db  = getDB();
        $ctx = $this->getContext();

        $record_id = (int)($_POST['record_id'] ?? 0);
        $month     = (int)($_POST['month'] ?? date('n'));
        $year      = (int)($_POST['year']  ?? date('Y'));

        $db->prepare('DELETE FROM payroll_records WHERE id=? AND restaurant_id=? AND status="draft"')
           ->execute([$record_id, $ctx['rest_id']]);

        $cqs = $ctx['cashier_id'] ? 'cashier_id=' . $ctx['cashier_id'] . '&' : '';
        header("Location: " . $this->baseUrl() . "/payroll?{$cqs}month={$month}&year={$year}&success=" . urlencode('Payroll record deleted.'));
        exit;
    }

    // ─── PAYSLIP ──────────────────────────────────────────────────────────────

    // ─── PAYROLL VIEW (dedicated view file) ──────────────────────────────────

    public function payrollPage() {
        $this->requireHr();
        FeatureGate::enforceSubscription($_SESSION['restaurant_id']);

        $db  = getDB();
        $ctx = $this->getContext();

        $month = (int)($_GET['month'] ?? date('n'));
        $year  = (int)($_GET['year']  ?? date('Y'));
        $month = max(1, min(12, $month));
        $year  = max(2020, min(2100, $year));

        $cashiers    = $ctx['is_admin'] ? $this->getCashiers($ctx['rest_id']) : [];
        $cashierName = null;
        if ($ctx['cashier_id']) {
            $cn = $db->prepare('SELECT name FROM users WHERE id = ?');
            $cn->execute([$ctx['cashier_id']]);
            $cashierName = $cn->fetchColumn();
        }

        $staff = $this->getStaff($ctx);
        $payrollMap = [];
        if (!empty($staff)) {
            $ids = array_column($staff, 'id');
            $ph  = implode(',', array_fill(0, count($ids), '?'));
            $pStmt = $db->prepare("SELECT * FROM payroll_records WHERE user_id IN ($ph) AND period_month=? AND period_year=?");
            $pStmt->execute(array_merge($ids, [$month, $year]));
            foreach ($pStmt->fetchAll() as $row) $payrollMap[$row['user_id']] = $row;
        }

        $currency  = FeatureGate::currency($ctx['rest_id']);
        $activeTab = 'payroll';
        $pageTitle = 'HR — Payroll';
        ob_start();
        require 'app/Views/hr/payroll.php';
        $content = ob_get_clean();
        $this->render($content, $pageTitle);
    }

    // ─── PAY ALL DRAFT ────────────────────────────────────────────────────────

    public function payAll() {
        $this->requireHr();
        $db  = getDB();
        $ctx = $this->getContext();
        $month = (int)($_POST['month'] ?? date('n'));
        $year  = (int)($_POST['year']  ?? date('Y'));

        $db->prepare("UPDATE payroll_records SET status='paid', paid_at=NOW() WHERE restaurant_id=? AND period_month=? AND period_year=? AND status='draft'")
           ->execute([$ctx['rest_id'], $month, $year]);

        $cqs = $ctx['cashier_id'] ? 'cashier_id=' . $ctx['cashier_id'] . '&' : '';
        header("Location: " . $this->baseUrl() . "/payroll?{$cqs}month={$month}&year={$year}&success=" . urlencode('All drafts marked as paid.'));
        exit;
    }



    // ─── ROLLBACK PAID TO DRAFT ───────────────────────────────────────────────

    public function markDraft() {
        $this->requireHr();
        $db  = getDB();
        $ctx = $this->getContext();

        $record_id = (int)($_POST['record_id'] ?? 0);
        $month     = (int)($_POST['month'] ?? date('n'));
        $year      = (int)($_POST['year']  ?? date('Y'));

        $stmt = $db->prepare('SELECT user_id FROM payroll_records WHERE id = ? AND restaurant_id = ?');
        $stmt->execute([$record_id, $ctx['rest_id']]);
        $record = $stmt->fetch();

        if ($record) {
            $db->prepare('UPDATE payroll_records SET status="draft", paid_at=NULL WHERE id=?')
               ->execute([$record_id]);

            // Sync profile salary to the now draft record
            $spStmt = $db->prepare('SELECT salary_type, base_salary, overtime_rate FROM staff_profiles WHERE user_id = ?');
            $spStmt->execute([$record['user_id']]);
            $sp = $spStmt->fetch();
            if ($sp) {
                $this->syncDraftPayroll($db, $record['user_id'], $ctx['rest_id'], $sp['salary_type'], $sp['base_salary'], $sp['overtime_rate']);
            }
        }

        $cqs = $ctx['cashier_id'] ? 'cashier_id=' . $ctx['cashier_id'] . '&' : '';
        header("Location: " . $this->baseUrl() . "/payroll?{$cqs}month={$month}&year={$year}&success=" . urlencode('Rollback to draft successful. Rates synced.'));
        exit;
    }

    // ─── PAYSLIP ──────────────────────────────────────────────────────────────

    public function payslip() {
        $this->requireHr();
        $db  = getDB();
        $ctx = $this->getContext();

        $record_id = (int)($_GET['id'] ?? 0);
        if (!$record_id) { header('Location: ' . $this->baseUrl() . '/payroll'); exit; }

        $rec = $db->prepare('
            SELECT pr.*, u.name, u.email, u.role,
                   sp.designation, sp.department, sp.phone,
                   r.name AS restaurant_name
            FROM payroll_records pr
            JOIN users u ON u.id = pr.user_id
            LEFT JOIN staff_profiles sp ON sp.user_id = pr.user_id
            JOIN restaurants r ON r.id = pr.restaurant_id
            WHERE pr.id = ? AND pr.restaurant_id = ?
        ');
        $rec->execute([$record_id, $ctx['rest_id']]);
        $record = $rec->fetch();
        if (!$record) { header('Location: ' . $this->baseUrl() . '/payroll'); exit; }

        $currency = FeatureGate::currency($ctx['rest_id']);
        require 'app/Views/hr/payslip.php';
        exit;
    }

    // ─── STORE CASHIER LOCATION ──────────────────────────────────────────────

    public function storeCashierLocation() {
        $this->requireHr();
        $db  = getDB();
        $ctx = $this->getContext();

        $cashier_id = (int)($_POST['cashier_id'] ?? 0);
        $latitude   = (float)($_POST['latitude']   ?? 0);
        $longitude  = (float)($_POST['longitude']  ?? 0);
        $radius     = (int)($_POST['radius']       ?? 100);

        if (!$cashier_id || !$latitude || !$longitude || $radius <= 0) {
            header("Location: " . $this->baseUrl() . "/attendance?error=" . urlencode('Invalid coordinates or radius.'));
            exit;
        }

        // Verify that this cashier belongs to this restaurant
        $check = $db->prepare('SELECT id FROM users WHERE id = ? AND restaurant_id = ? AND role = "cashier"');
        $check->execute([$cashier_id, $ctx['rest_id']]);
        if (!$check->fetch()) {
            header("Location: " . $this->baseUrl() . "/attendance?error=" . urlencode('Cashier branch not found.'));
            exit;
        }

        $db->prepare("
            INSERT INTO cashier_locations (restaurant_id, cashier_id, latitude, longitude, radius)
            VALUES (?, ?, ?, ?, ?)
            ON DUPLICATE KEY UPDATE latitude = VALUES(latitude), longitude = VALUES(longitude), radius = VALUES(radius)
        ")->execute([$ctx['rest_id'], $cashier_id, $latitude, $longitude, $radius]);

        header("Location: " . $this->baseUrl() . "/attendance?success=" . urlencode('Cashier geofencing settings saved.'));
        exit;
    }

    // ─── PUBLIC CLOCK PAGE ───────────────────────────────────────────────────

    public function showClockPage() {
        $db = getDB();
        $cashier_id = (int)($_GET['cashier_id'] ?? 0);

        if (!$cashier_id) {
            echo "<h1>Error: Missing Cashier ID</h1><p>Please scan a valid branch QR code.</p>";
            exit;
        }

        // Fetch cashier and geofencing info
        $cStmt = $db->prepare("
            SELECT u.id, u.name, cl.latitude, cl.longitude, cl.radius, r.name AS rest_name, r.currency
            FROM users u
            JOIN restaurants r ON r.id = u.restaurant_id
            LEFT JOIN cashier_locations cl ON cl.cashier_id = u.id
            WHERE u.id = ? AND u.role = 'cashier'
        ");
        $cStmt->execute([$cashier_id]);
        $cashier = $cStmt->fetch();

        if (!$cashier) {
            echo "<h1>Error: Invalid Cashier Branch</h1><p>Please contact HR.</p>";
            exit;
        }

        require 'app/Views/hr/clock.php';
        exit;
    }

    // ─── SHOW PRINTABLE QR CARD ──────────────────────────────────────────────

    public function showQrCard() {
        $this->requireHr();
        $db = getDB();
        $cashier_id = (int)($_GET['id'] ?? 0);

        if (!$cashier_id) {
            echo "<h1>Error: Missing Cashier ID</h1>";
            exit;
        }

        // Fetch cashier and restaurant name
        $cStmt = $db->prepare("
            SELECT u.id, u.name, r.name AS rest_name, r.logo
            FROM users u
            JOIN restaurants r ON r.id = u.restaurant_id
            WHERE u.id = ? AND u.restaurant_id = ? AND u.role = 'cashier'
        ");
        $cStmt->execute([$cashier_id, $_SESSION['restaurant_id']]);
        $cashier = $cStmt->fetch();

        if (!$cashier) {
            echo "<h1>Error: Cashier Branch not found.</h1>";
            exit;
        }

        $clockUrl = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'] . '/clock?cashier_id=' . $cashier['id'];
        
        require 'app/Views/hr/qr_card.php';
        exit;
    }

    // ─── PUBLIC CLOCK SUBMIT ─────────────────────────────────────────────────

    public function submitClock() {
        $db = getDB();

        $cashier_id = (int)($_POST['cashier_id'] ?? 0);
        $pin        = trim($_POST['pin']         ?? '');
        $lat        = (float)($_POST['latitude']  ?? 0);
        $lng        = (float)($_POST['longitude'] ?? 0);

        if (!$cashier_id || !$pin) {
            echo json_encode(['success' => false, 'message' => 'Cashier ID and PIN are required.']);
            exit;
        }

        // 1. Fetch Cashier & Geofence
        $cStmt = $db->prepare("SELECT * FROM cashier_locations WHERE cashier_id = ?");
        $cStmt->execute([$cashier_id]);
        $loc = $cStmt->fetch();

        if (!$loc) {
            echo json_encode(['success' => false, 'message' => 'This cashier branch does not have geofencing configured by HR.']);
            exit;
        }

        // 2. Fetch Employee by PIN
        $spStmt = $db->prepare("
            SELECT sp.user_id, sp.restaurant_id, sp.shift_id, u.name, u.status
            FROM staff_profiles sp
            JOIN users u ON u.id = sp.user_id
            WHERE sp.clock_in_pin = ? AND sp.restaurant_id = ? AND u.status = 'active'
        ");
        // Get the restaurant ID of the cashier to scope the pin search
        $restStmt = $db->prepare("SELECT restaurant_id FROM users WHERE id = ?");
        $restStmt->execute([$cashier_id]);
        $cashierRestId = $restStmt->fetchColumn();

        if (!$cashierRestId) {
            echo json_encode(['success' => false, 'message' => 'Invalid branch.']);
            exit;
        }

        $spStmt->execute([$pin, $cashierRestId]);
        $staff = $spStmt->fetch();

        if (!$staff) {
            echo json_encode(['success' => false, 'message' => 'Invalid PIN. Please check with HR.']);
            exit;
        }

        $uid     = (int)$staff['user_id'];
        $restId  = (int)$staff['restaurant_id'];

        // 3. Geofence Verification (Haversine formula)
        $earthRadius = 6371000; // in meters
        $latFrom = deg2rad($lat);
        $lonFrom = deg2rad($lng);
        $latTo   = deg2rad($loc['latitude']);
        $lonTo   = deg2rad($loc['longitude']);

        $latDelta = $latTo - $latFrom;
        $lonDelta = $lonTo - $lonFrom;

        $angle = 2 * asin(sqrt(pow(sin($latDelta / 2), 2) +
            cos($latFrom) * cos($latTo) * pow(sin($lonDelta / 2), 2)));
        $distance = $angle * $earthRadius;

        if ($distance > $loc['radius']) {
            echo json_encode([
                'success' => false,
                'message' => sprintf('Location Blocked: You are %.1f meters away. Max allowed distance is %d meters.', $distance, $loc['radius'])
            ]);
            exit;
        }

        // 4. Clock-in or Clock-out Action
        $nowRow = $db->query("SELECT CURDATE() as cur_date, CURTIME() as cur_time")->fetch();
        $date = $nowRow['cur_date'];
        $time = $nowRow['cur_time'];

        // Check if there is an existing log for today
        $logStmt = $db->prepare("SELECT * FROM attendance_logs WHERE user_id = ? AND log_date = ?");
        $logStmt->execute([$uid, $date]);
        $log = $logStmt->fetch();

        // Load shift for smart status calculations
        $shStmt = $db->prepare("SELECT * FROM shift_schedules WHERE id = ?");
        $shStmt->execute([$staff['shift_id']]);
        $shift = $shStmt->fetch();

        if (!$log) {
            // CLOCK IN
            $status = 'present';
            if ($shift && $shift['start_time']) {
                $shiftStart = strtotime($date . ' ' . $shift['start_time']);
                $checkInTs  = strtotime($date . ' ' . $time);
                $lateBuffer = ($shift['late_threshold_minutes'] ?? 15) * 60;
                if ($checkInTs > $shiftStart + $lateBuffer) {
                    $status = 'late';
                }
            }

            $ins = $db->prepare("
                INSERT INTO attendance_logs (user_id, restaurant_id, cashier_id, log_date, check_in, status, marked_by)
                VALUES (?, ?, ?, ?, ?, ?, ?)
            ");
            $ins->execute([$uid, $restId, $cashier_id, $date, $time, $status, $uid]);

            echo json_encode([
                'success' => true,
                'action' => 'in',
                'message' => sprintf('Good day %s! Successfully clocked in at %s (Status: %s).', $staff['name'], date('g:i A', strtotime($time)), ucfirst($status))
            ]);
            exit;
        } else {
            // CLOCK OUT
            if ($log['check_out']) {
                echo json_encode(['success' => false, 'message' => 'You have already clocked out for today.']);
                exit;
            }

            $checkInTs  = strtotime($date . ' ' . $log['check_in']);
            $checkOutTs = strtotime($date . ' ' . $time);
            $hoursWorked = 0;
            if ($checkOutTs > $checkInTs) {
                $hoursWorked = round(($checkOutTs - $checkInTs) / 3600, 2);
            }

            $status   = $log['status'];
            $earlyOut = $log['early_out'];

            if ($shift && $shift['end_time']) {
                $shiftEnd    = strtotime($date . ' ' . $shift['end_time']);
                $earlyBuffer = ($shift['early_out_threshold_minutes'] ?? 15) * 60;
                if ($checkOutTs < $shiftEnd - $earlyBuffer) {
                    $earlyOut = 1;
                    if ($shift['start_time']) {
                        $shiftHours = ($shiftEnd - strtotime($date . ' ' . $shift['start_time'])) / 3600;
                        if ($hoursWorked < ($shiftHours * 0.5)) {
                            $status = 'half_day';
                        }
                    }
                }
            }

            $upd = $db->prepare("
                UPDATE attendance_logs
                SET check_out = ?, hours_worked = ?, early_out = ?, status = ?
                WHERE id = ?
            ");
            $upd->execute([$time, $hoursWorked, $earlyOut, $status, $log['id']]);

            echo json_encode([
                'success' => true,
                'action' => 'out',
                'message' => sprintf('Good bye %s! Successfully clocked out at %s. Worked: %.2f hours.', $staff['name'], date('g:i A', strtotime($time)), $hoursWorked)
            ]);
            exit;
        }
    }

    // ─── Render helper ────────────────────────────────────────────────────────


    private function render($content, $pageTitle) {
        if ($_SESSION['role'] === 'hr_manager') {
            require 'app/Views/layouts/hr.php';
        } else {
            require 'app/Views/layouts/admin.php';
        }
    }
}


