query("SELECT NOW() AS now, CURDATE() AS today, YEAR(NOW()) AS y, MONTH(NOW()) AS m")->fetch(); return [ 'now' => $row['now'], 'today' => $row['today'], 'year' => (int)$row['y'], 'month' => (int)$row['m'], ]; } function roundUpTo15Minutes(string $datetime): string { $timestamp = strtotime($datetime); $minutes = (int)date('i', $timestamp); $remainder = $minutes % 15; if ($remainder === 0) { $roundedMinutes = $minutes; } else { $roundedMinutes = $minutes + (15 - $remainder); } $baseTime = strtotime(date('Y-m-d H:00:00', $timestamp)); return date('Y-m-d H:i:s', $baseTime + ($roundedMinutes * 60)); } function roundDownTo15Minutes(string $datetime): string { $timestamp = strtotime($datetime); $minutes = (int)date('i', $timestamp); $remainder = $minutes % 15; $roundedMinutes = $minutes - $remainder; $baseTime = strtotime(date('Y-m-d H:00:00', $timestamp)); return date('Y-m-d H:i:s', $baseTime + ($roundedMinutes * 60)); } function roundToNearest10Minutes(string $datetime): string { $timestamp = strtotime($datetime); $minutes = (int)date('i', $timestamp); $remainder = $minutes % 10; if ($remainder < 5) { $roundedMinutes = $minutes - $remainder; } else { $roundedMinutes = $minutes + (10 - $remainder); } $baseTime = strtotime(date('Y-m-d H:00:00', $timestamp)); return date('Y-m-d H:i:s', $baseTime + ($roundedMinutes * 60)); } /** * @param array $record */ function calculateWorkMinutes(array $record): int { if (!$record['arrival_time'] || !$record['departure_time']) { return 0; } $arrival = strtotime($record['arrival_time']); $departure = strtotime($record['departure_time']); $totalMinutes = ($departure - $arrival) / 60; if ($record['break_start'] && $record['break_end']) { $breakStart = strtotime($record['break_start']); $breakEnd = strtotime($record['break_end']); $breakMinutes = ($breakEnd - $breakStart) / 60; $totalMinutes -= $breakMinutes; } return max(0, (int)$totalMinutes); } /** * @return array{vacation_total: float, vacation_used: float, vacation_remaining: float, sick_used: float} */ function getLeaveBalance(PDO $pdo, int $userId, ?int $year = null): array { $year = $year ?: (int)date('Y'); $stmt = $pdo->prepare( 'SELECT 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'], ]; } /** * Batch get leave balances for multiple users (eliminates N+1 queries) * * @param array $userIds * @return array */ function getLeaveBalancesBatch(PDO $pdo, array $userIds, ?int $year = null): array { $year = $year ?: (int)date('Y'); $result = []; $default = [ 'vacation_total' => 160, 'vacation_used' => 0, 'vacation_remaining' => 160, 'sick_used' => 0, ]; if (empty($userIds)) { return $result; } $placeholders = implode(',', array_fill(0, count($userIds), '?')); $stmt = $pdo->prepare(" SELECT user_id, vacation_total, vacation_used, sick_used FROM leave_balances WHERE user_id IN ($placeholders) AND year = ? "); $params = array_values($userIds); $params[] = $year; $stmt->execute($params); $rows = $stmt->fetchAll(); $balanceMap = []; foreach ($rows as $row) { $balanceMap[$row['user_id']] = [ 'vacation_total' => (float)$row['vacation_total'], 'vacation_used' => (float)$row['vacation_used'], 'vacation_remaining' => (float)$row['vacation_total'] - (float)$row['vacation_used'], 'sick_used' => (float)$row['sick_used'], ]; } foreach ($userIds as $uid) { $result[$uid] = $balanceMap[$uid] ?? $default; } return $result; } function updateLeaveBalance(PDO $pdo, int $userId, string $date, string $leaveType, float $hours): void { if ($leaveType === 'work' || $leaveType === 'holiday' || $leaveType === 'unpaid') { return; } $year = (int)date('Y', strtotime($date)); $stmt = $pdo->prepare('SELECT id FROM leave_balances WHERE user_id = ? AND year = ?'); $stmt->execute([$userId, $year]); $balance = $stmt->fetch(); if (!$balance) { $stmt = $pdo->prepare( 'INSERT INTO leave_balances (user_id, year, vacation_total, vacation_used, sick_used) VALUES (?, ?, 160, 0, 0)' ); $stmt->execute([$userId, $year]); } if ($leaveType === 'vacation') { $stmt = $pdo->prepare( 'UPDATE leave_balances SET vacation_used = vacation_used + ? WHERE user_id = ? AND year = ?' ); $stmt->execute([$hours, $userId, $year]); } elseif ($leaveType === 'sick') { $stmt = $pdo->prepare('UPDATE leave_balances SET sick_used = sick_used + ? WHERE user_id = ? AND year = ?'); $stmt->execute([$hours, $userId, $year]); } } function getCzechMonthName(int $month): string { $months = [ 1 => 'Leden', 2 => 'Únor', 3 => 'Březen', 4 => 'Duben', 5 => 'Květen', 6 => 'Červen', 7 => 'Červenec', 8 => 'Srpen', 9 => 'Září', 10 => 'Říjen', 11 => 'Listopad', 12 => 'Prosinec', ]; return $months[$month] ?? ''; } function getCzechDayName(int $dayOfWeek): string { $days = [ 0 => 'neděle', 1 => 'pondělí', 2 => 'úterý', 3 => 'středa', 4 => 'čtvrtek', 5 => 'pátek', 6 => 'sobota', ]; return $days[$dayOfWeek] ?? ''; } /** * Enrich attendance records with project logs and project names (in-place) * * @param array> $records */ function enrichRecordsWithProjectLogs(PDO $pdo, array &$records): void { $recordIds = array_column($records, 'id'); $recordProjectLogs = []; if (!empty($recordIds)) { $placeholders = implode(',', array_fill(0, count($recordIds), '?')); $stmt = $pdo->prepare( "SELECT id, attendance_id, project_id, started_at, ended_at, hours, minutes FROM attendance_project_logs WHERE attendance_id IN ($placeholders) ORDER BY started_at ASC" ); $stmt->execute($recordIds); foreach ($stmt->fetchAll() as $log) { $recordProjectLogs[$log['attendance_id']][] = $log; } } $projectIds = []; foreach ($records as $rec) { if ($rec['project_id']) { $projectIds[$rec['project_id']] = $rec['project_id']; } } foreach ($recordProjectLogs as $logs) { foreach ($logs as $l) { $projectIds[$l['project_id']] = $l['project_id']; } } $projectNameMap = fetchProjectNames($projectIds); foreach ($records as &$rec) { $rec['project_name'] = $rec['project_id'] ? ($projectNameMap[$rec['project_id']] ?? null) : null; $logs = $recordProjectLogs[$rec['id']] ?? []; foreach ($logs as &$l) { $l['project_name'] = $projectNameMap[$l['project_id']] ?? null; } unset($l); $rec['project_logs'] = $logs; } unset($rec); } /** * Calculate per-user totals from records array * * @param list> $records * @return array> */ function calculateUserTotals(array $records, bool $includeRecords = false): array { $userTotals = []; foreach ($records as $record) { $uid = $record['user_id']; if (!isset($userTotals[$uid])) { $userTotals[$uid] = [ 'name' => $record['user_name'], 'minutes' => 0, 'working' => false, 'vacation_hours' => 0, 'sick_hours' => 0, 'holiday_hours' => 0, 'unpaid_hours' => 0, ]; if ($includeRecords) { $userTotals[$uid]['records'] = []; } } $leaveType = $record['leave_type'] ?? 'work'; $leaveHours = (float)($record['leave_hours'] ?? 0); if ($leaveType === 'vacation') { $userTotals[$uid]['vacation_hours'] += $leaveHours; } elseif ($leaveType === 'sick') { $userTotals[$uid]['sick_hours'] += $leaveHours; } elseif ($leaveType === 'holiday') { $userTotals[$uid]['holiday_hours'] += $leaveHours; } elseif ($leaveType === 'unpaid') { $userTotals[$uid]['unpaid_hours'] += $leaveHours; } else { $userTotals[$uid]['minutes'] += calculateWorkMinutes($record); } if ($includeRecords) { $userTotals[$uid]['records'][] = $record; } if ($record['arrival_time'] && !$record['departure_time']) { $userTotals[$uid]['working'] = true; } } return $userTotals; } /** * Add monthly fund data and "working now" status to user totals * * @param array> $userTotals */ function addFundDataToUserTotals(PDO $pdo, array &$userTotals, int $year, int $monthNum): void { $fund = CzechHolidays::getMonthlyWorkFund($year, $monthNum); $businessDays = CzechHolidays::getBusinessDaysInMonth($year, $monthNum); foreach ($userTotals as $uid => &$ut) { $workedHours = round($ut['minutes'] / 60, 1); $leaveHours = $ut['vacation_hours'] + $ut['sick_hours']; $covered = $workedHours + $leaveHours; $ut['fund'] = $fund; $ut['business_days'] = $businessDays; $ut['worked_hours'] = $workedHours; $ut['covered'] = $covered; $ut['missing'] = max(0, round($fund - $covered, 1)); $ut['overtime'] = max(0, round($covered - $fund, 1)); } unset($ut); $stmt = $pdo->prepare(" SELECT DISTINCT user_id FROM attendance WHERE shift_date = CURDATE() AND arrival_time IS NOT NULL AND departure_time IS NULL AND (leave_type IS NULL OR leave_type = 'work') "); $stmt->execute(); $workingNow = $stmt->fetchAll(PDO::FETCH_COLUMN); foreach ($workingNow as $uid) { if (isset($userTotals[$uid])) { $userTotals[$uid]['working'] = true; } } } /** * Fetch project names from offers DB * * @param array $projectIds * @return array */ function fetchProjectNames(array $projectIds): array { if (empty($projectIds)) { return []; } try { $pdo = db(); $placeholders = implode(',', array_fill(0, count($projectIds), '?')); $stmt = $pdo->prepare("SELECT id, project_number, name FROM projects WHERE id IN ($placeholders)"); $stmt->execute(array_values($projectIds)); $map = []; foreach ($stmt->fetchAll() as $p) { $map[$p['id']] = $p['project_number'] . ' – ' . $p['name']; } return $map; } catch (\Exception $e) { error_log('Failed to fetch project names: ' . $e->getMessage()); return []; } }