modify('+1 day'); // include the end date $days = 0; $period = new DatePeriod($start, new DateInterval('P1D'), $end); foreach ($period as $date) { $dayOfWeek = (int)$date->format('N'); // 1=Mon, 7=Sun if ($dayOfWeek <= 5) { $days++; } } return $days; } /** * Get leave balance for user (reuse logic from attendance.php) * * @return array */ function getLeaveBalanceForRequest(PDO $pdo, int $userId, ?int $year = null): array { $year = $year ?: (int)date('Y'); $stmt = $pdo->prepare( 'SELECT id, user_id, year, vacation_total, vacation_used, sick_used FROM leave_balances WHERE user_id = ? AND year = ?' ); $stmt->execute([$userId, $year]); $balance = $stmt->fetch(); if (!$balance) { return [ 'vacation_total' => 160, 'vacation_used' => 0, 'vacation_remaining' => 160, 'sick_used' => 0, ]; } return [ 'vacation_total' => (float)$balance['vacation_total'], 'vacation_used' => (float)$balance['vacation_used'], 'vacation_remaining' => (float)$balance['vacation_total'] - (float)$balance['vacation_used'], 'sick_used' => (float)$balance['sick_used'], ]; } /** * Get hours already locked in pending requests for vacation */ function getPendingVacationHours(PDO $pdo, int $userId, int $year): float { $stmt = $pdo->prepare(" SELECT COALESCE(SUM(total_hours), 0) as pending_hours FROM leave_requests WHERE user_id = ? AND leave_type = 'vacation' AND status = 'pending' AND YEAR(date_from) = ? "); $stmt->execute([$userId, $year]); return (float)$stmt->fetchColumn(); } // ============================================================================ // GET Handlers // ============================================================================ /** * GET - Own leave requests */ function handleGetMyRequests(PDO $pdo, int $userId): void { $stmt = $pdo->prepare(" SELECT lr.id, lr.user_id, lr.leave_type, lr.date_from, lr.date_to, lr.total_hours, lr.total_days, lr.notes, lr.status, lr.reviewer_id, lr.reviewer_note, lr.reviewed_at, lr.created_at, CONCAT(u.first_name, ' ', u.last_name) as reviewer_name FROM leave_requests lr LEFT JOIN users u ON lr.reviewer_id = u.id WHERE lr.user_id = ? ORDER BY lr.created_at DESC "); $stmt->execute([$userId]); $requests = $stmt->fetchAll(); successResponse($requests); } /** * GET - All pending requests (for approver) */ function handleGetPending(PDO $pdo): void { $stmt = $pdo->prepare(" SELECT lr.id, lr.user_id, lr.leave_type, lr.date_from, lr.date_to, lr.total_hours, lr.total_days, lr.notes, lr.status, lr.reviewer_id, lr.reviewer_note, lr.reviewed_at, lr.created_at, CONCAT(u.first_name, ' ', u.last_name) as employee_name, CONCAT(rv.first_name, ' ', rv.last_name) as reviewer_name FROM leave_requests lr JOIN users u ON lr.user_id = u.id LEFT JOIN users rv ON lr.reviewer_id = rv.id WHERE lr.status = 'pending' ORDER BY lr.created_at ASC "); $stmt->execute(); $requests = $stmt->fetchAll(); successResponse([ 'requests' => $requests, 'count' => count($requests), ]); } /** * GET - All requests with filters (for approver) */ function handleGetAll(PDO $pdo): void { $status = $_GET['status'] ?? ''; $userId = isset($_GET['user_id']) ? (int)$_GET['user_id'] : null; $where = []; $params = []; if ($status && in_array($status, ['pending', 'approved', 'rejected', 'cancelled'])) { $where[] = 'lr.status = ?'; $params[] = $status; } if ($userId) { $where[] = 'lr.user_id = ?'; $params[] = $userId; } $whereClause = $where ? 'WHERE ' . implode(' AND ', $where) : ''; $stmt = $pdo->prepare(" SELECT lr.id, lr.user_id, lr.leave_type, lr.date_from, lr.date_to, lr.total_hours, lr.total_days, lr.notes, lr.status, lr.reviewer_id, lr.reviewer_note, lr.reviewed_at, lr.created_at, CONCAT(u.first_name, ' ', u.last_name) as employee_name, CONCAT(rv.first_name, ' ', rv.last_name) as reviewer_name FROM leave_requests lr JOIN users u ON lr.user_id = u.id LEFT JOIN users rv ON lr.reviewer_id = rv.id $whereClause ORDER BY lr.created_at DESC LIMIT 200 "); $stmt->execute($params); $requests = $stmt->fetchAll(); successResponse($requests); } // ============================================================================ // POST Handlers // ============================================================================ /** * POST - Submit new leave request */ function handleSubmitRequest(PDO $pdo, int $userId): void { $input = getJsonInput(); $leaveType = $input['leave_type'] ?? ''; $dateFrom = $input['date_from'] ?? ''; $dateTo = $input['date_to'] ?? ''; $notes = trim($input['notes'] ?? ''); if (!$leaveType || !$dateFrom || !$dateTo) { errorResponse('Vyplňte všechna povinná pole'); } if (!in_array($leaveType, ['vacation', 'sick', 'unpaid'])) { errorResponse('Neplatný typ nepřítomnosti'); } // Validate dates $from = new DateTime($dateFrom); $to = new DateTime($dateTo); if ($to < $from) { errorResponse('Datum "do" nesmí být před datem "od"'); } // Calculate business days $businessDays = calculateBusinessDays($dateFrom, $dateTo); if ($businessDays === 0) { errorResponse('Zvolené období neobsahuje žádné pracovní dny'); } $totalHours = $businessDays * 8; // Check vacation balance if ($leaveType === 'vacation') { $year = (int)$from->format('Y'); $balance = getLeaveBalanceForRequest($pdo, $userId, $year); $pendingHours = getPendingVacationHours($pdo, $userId, $year); $availableHours = $balance['vacation_remaining'] - $pendingHours; if ($availableHours < $totalHours) { errorResponse( "Nemáte dostatek hodin dovolené. Dostupné: {$availableHours}h" . " (zbývá {$balance['vacation_remaining']}h, v čekajících žádostech: {$pendingHours}h)," . " požadujete: {$totalHours}h." ); } } // Check overlapping requests $stmt = $pdo->prepare(" SELECT id FROM leave_requests WHERE user_id = ? AND status IN ('pending', 'approved') AND date_from <= ? AND date_to >= ? "); $stmt->execute([$userId, $dateTo, $dateFrom]); if ($stmt->fetch()) { errorResponse('Pro toto období již existuje žádost o nepřítomnost'); } // Insert request $stmt = $pdo->prepare(" INSERT INTO leave_requests (user_id, leave_type, date_from, date_to, total_hours, total_days, notes, status) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending') "); $stmt->execute([$userId, $leaveType, $dateFrom, $dateTo, $totalHours, $businessDays, $notes ?: null]); $requestId = (int)$pdo->lastInsertId(); AuditLog::logCreate('leave_request', $requestId, [ 'leave_type' => $leaveType, 'date_from' => $dateFrom, 'date_to' => $dateTo, 'total_days' => $businessDays, 'total_hours' => $totalHours, ], "Podána žádost o nepřítomnost: $leaveType ($dateFrom - $dateTo)"); // Send email notification try { $stmt = $pdo->prepare("SELECT CONCAT(first_name, ' ', last_name) as name FROM users WHERE id = ?"); $stmt->execute([$userId]); $employeeName = $stmt->fetchColumn() ?: 'Neznámý'; LeaveNotification::notifyNewRequest([ 'leave_type' => $leaveType, 'date_from' => $dateFrom, 'date_to' => $dateTo, 'total_days' => $businessDays, 'total_hours' => $totalHours, 'notes' => $notes, ], $employeeName); } catch (\Exception $e) { error_log('Leave notification error: ' . $e->getMessage()); } successResponse(['id' => $requestId], 'Žádost byla odeslána ke schválení'); } /** * POST - Cancel own pending request */ function handleCancelRequest(PDO $pdo, int $userId): void { $input = getJsonInput(); $requestId = (int)($input['request_id'] ?? 0); if (!$requestId) { errorResponse('ID žádosti je povinné'); } $stmt = $pdo->prepare( 'SELECT id, user_id, leave_type, date_from, date_to, total_hours, total_days, notes, status FROM leave_requests WHERE id = ? AND user_id = ?' ); $stmt->execute([$requestId, $userId]); $request = $stmt->fetch(); if (!$request) { errorResponse('Žádost nebyla nalezena'); } if ($request['status'] !== 'pending') { errorResponse('Lze zrušit pouze čekající žádosti'); } $stmt = $pdo->prepare("UPDATE leave_requests SET status = 'cancelled' WHERE id = ?"); $stmt->execute([$requestId]); AuditLog::logUpdate( 'leave_request', $requestId, ['status' => 'pending'], ['status' => 'cancelled'], 'Žádost o nepřítomnost zrušena zaměstnancem' ); successResponse(null, 'Žádost byla zrušena'); } /** * POST - Approve a leave request * * @param array $authData */ function handleApproveRequest(PDO $pdo, int $reviewerId, array $authData): void { $input = getJsonInput(); $requestId = (int)($input['request_id'] ?? 0); if (!$requestId) { errorResponse('ID žádosti je povinné'); } $stmt = $pdo->prepare( 'SELECT id, user_id, leave_type, date_from, date_to, total_hours, total_days, status FROM leave_requests WHERE id = ?' ); $stmt->execute([$requestId]); $request = $stmt->fetch(); if (!$request) { errorResponse('Žádost nebyla nalezena'); } if ($request['status'] !== 'pending') { errorResponse('Lze schválit pouze čekající žádosti'); } if ((int)$request['user_id'] === $reviewerId && !($authData['user']['is_admin'] ?? false)) { errorResponse('Nemůžete schválit vlastní žádost', 403); } // Re-check vacation balance if ($request['leave_type'] === 'vacation') { $year = (int)date('Y', strtotime($request['date_from'])); $balance = getLeaveBalanceForRequest($pdo, (int)$request['user_id'], $year); if ($balance['vacation_remaining'] < (float)$request['total_hours']) { errorResponse( "Zaměstnanec nemá dostatek hodin dovolené." . " Zbývá: {$balance['vacation_remaining']}h, požadováno: {$request['total_hours']}h." ); } } // Begin transaction $pdo->beginTransaction(); try { // Create attendance records for each business day $start = new DateTime($request['date_from']); $end = new DateTime($request['date_to']); $end->modify('+1 day'); $period = new DatePeriod($start, new DateInterval('P1D'), $end); $insertStmt = $pdo->prepare(' INSERT INTO attendance (user_id, shift_date, leave_type, leave_hours, notes) VALUES (?, ?, ?, 8, ?) '); $leaveNote = "Schválená žádost #$requestId"; $totalBusinessDays = 0; foreach ($period as $date) { $dayOfWeek = (int)$date->format('N'); if ($dayOfWeek <= 5) { $shiftDate = $date->format('Y-m-d'); $insertStmt->execute([ $request['user_id'], $shiftDate, $request['leave_type'], $leaveNote, ]); $totalBusinessDays++; } } // Update leave balance ONCE with total hours (was N queries, one per day) if ($totalBusinessDays > 0) { updateLeaveBalance( $pdo, (int)$request['user_id'], $request['date_from'], $request['leave_type'], (float)($totalBusinessDays * 8) ); } // Update request status $stmt = $pdo->prepare(" UPDATE leave_requests SET status = 'approved', reviewer_id = ?, reviewed_at = NOW() WHERE id = ? "); $stmt->execute([$reviewerId, $requestId]); $pdo->commit(); AuditLog::logUpdate( 'leave_request', $requestId, ['status' => 'pending'], ['status' => 'approved', 'reviewer_id' => $reviewerId], 'Žádost o nepřítomnost schválena' ); successResponse(null, 'Žádost byla schválena'); } catch (\Exception $e) { $pdo->rollBack(); error_log('Approve request error: ' . $e->getMessage()); errorResponse('Chyba při schvalování žádosti', 500); } } /** * POST - Reject a leave request * * @param array $authData */ function handleRejectRequest(PDO $pdo, int $reviewerId, array $authData): void { $input = getJsonInput(); $requestId = (int)($input['request_id'] ?? 0); $note = trim($input['note'] ?? ''); if (!$requestId) { errorResponse('ID žádosti je povinné'); } if (!$note) { errorResponse('Důvod zamítnutí je povinný'); } $stmt = $pdo->prepare( 'SELECT id, user_id, status FROM leave_requests WHERE id = ?' ); $stmt->execute([$requestId]); $request = $stmt->fetch(); if (!$request) { errorResponse('Žádost nebyla nalezena'); } if ($request['status'] !== 'pending') { errorResponse('Lze zamítnout pouze čekající žádosti'); } if ((int)$request['user_id'] === $reviewerId && !($authData['user']['is_admin'] ?? false)) { errorResponse('Nemůžete zamítnout vlastní žádost', 403); } $stmt = $pdo->prepare(" UPDATE leave_requests SET status = 'rejected', reviewer_id = ?, reviewer_note = ?, reviewed_at = NOW() WHERE id = ? "); $stmt->execute([$reviewerId, $note, $requestId]); AuditLog::logUpdate( 'leave_request', $requestId, ['status' => 'pending'], ['status' => 'rejected', 'reviewer_id' => $reviewerId, 'reviewer_note' => $note], "Žádost o nepřítomnost zamítnuta: $note" ); successResponse(null, 'Žádost byla zamítnuta'); }