prepare(" SELECT id, user_id, shift_date, arrival_time, arrival_lat, arrival_lng, arrival_accuracy, arrival_address, break_start, break_end, departure_time, departure_lat, departure_lng, departure_accuracy, departure_address, notes, project_id, leave_type, leave_hours, created_at FROM attendance WHERE user_id = ? AND departure_time IS NULL AND (leave_type IS NULL OR leave_type = 'work') ORDER BY created_at DESC LIMIT 1 "); $stmt->execute([$userId]); $ongoingShift = $stmt->fetch(); $projectLogs = []; $activeProjectId = null; if ($ongoingShift) { $stmt = $pdo->prepare( 'SELECT id, attendance_id, project_id, started_at, ended_at, hours, minutes FROM attendance_project_logs WHERE attendance_id = ? ORDER BY started_at ASC' ); $stmt->execute([$ongoingShift['id']]); $projectLogs = $stmt->fetchAll(); foreach ($projectLogs as $log) { if ($log['ended_at'] === null) { $activeProjectId = (int)$log['project_id']; break; } } } $stmt = $pdo->prepare(" SELECT id, user_id, shift_date, arrival_time, arrival_lat, arrival_lng, arrival_accuracy, arrival_address, break_start, break_end, departure_time, departure_lat, departure_lng, departure_accuracy, departure_address, notes, project_id, leave_type, leave_hours, created_at FROM attendance WHERE user_id = ? AND shift_date = ? AND departure_time IS NOT NULL AND (leave_type IS NULL OR leave_type = 'work') ORDER BY arrival_time DESC "); $stmt->execute([$userId, $today]); $todayShifts = $stmt->fetchAll(); $completedShiftIds = array_column($todayShifts, 'id'); $completedProjectLogs = []; if (!empty($completedShiftIds)) { $placeholders = implode(',', array_fill(0, count($completedShiftIds), '?')); $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($completedShiftIds); $allLogs = $stmt->fetchAll(); foreach ($allLogs as $log) { $completedProjectLogs[$log['attendance_id']][] = $log; } } $leaveBalance = getLeaveBalance($pdo, $userId); $currentYear = (int)date('Y'); $currentMonth = (int)date('m'); $fund = CzechHolidays::getMonthlyWorkFund($currentYear, $currentMonth); $businessDays = CzechHolidays::getBusinessDaysInMonth($currentYear, $currentMonth); $startDate = date('Y-m-01'); $endDate = date('Y-m-t'); $stmt = $pdo->prepare(' SELECT id, user_id, shift_date, arrival_time, break_start, break_end, departure_time, notes, project_id, leave_type, leave_hours FROM attendance WHERE user_id = ? AND shift_date BETWEEN ? AND ? '); $stmt->execute([$userId, $startDate, $endDate]); $monthRecords = $stmt->fetchAll(); $workedMinutes = 0; $leaveHoursMonth = 0; $vacationHours = 0; $sickHours = 0; $holidayHours = 0; $unpaidHours = 0; foreach ($monthRecords as $rec) { $lt = $rec['leave_type'] ?? 'work'; $lh = (float)($rec['leave_hours'] ?? 0); if ($lt === 'work') { if ($rec['departure_time']) { $workedMinutes += calculateWorkMinutes($rec); } } elseif ($lt === 'vacation') { $vacationHours += $lh; $leaveHoursMonth += $lh; } elseif ($lt === 'sick') { $sickHours += $lh; $leaveHoursMonth += $lh; } elseif ($lt === 'holiday') { $holidayHours += $lh; } elseif ($lt === 'unpaid') { $unpaidHours += $lh; } } $workedHours = round($workedMinutes / 60, 1); $covered = $workedHours + $leaveHoursMonth; $remaining = max(0, $fund - $covered); $overtime = max(0, round($covered - $fund, 1)); $monthlyFund = [ 'fund' => $fund, 'business_days' => $businessDays, 'worked' => $workedHours, 'leave_hours' => $leaveHoursMonth, 'vacation_hours' => $vacationHours, 'sick_hours' => $sickHours, 'holiday_hours' => $holidayHours, 'unpaid_hours' => $unpaidHours, 'covered' => $covered, 'remaining' => $remaining, 'overtime' => $overtime, 'month_name' => getCzechMonthName($currentMonth) . ' ' . $currentYear, ]; // Enrich project logs with names $allLogProjectIds = []; foreach ($projectLogs as $l) { $allLogProjectIds[$l['project_id']] = $l['project_id']; } foreach ($completedProjectLogs as $logs) { foreach ($logs as $l) { $allLogProjectIds[$l['project_id']] = $l['project_id']; } } $projNameMap = fetchProjectNames($allLogProjectIds); foreach ($projectLogs as &$l) { $l['project_name'] = $projNameMap[$l['project_id']] ?? null; } unset($l); foreach ($completedProjectLogs as &$logs) { foreach ($logs as &$l) { $l['project_name'] = $projNameMap[$l['project_id']] ?? null; } unset($l); } unset($logs); foreach ($todayShifts as &$shift) { $shift['project_logs'] = $completedProjectLogs[$shift['id']] ?? []; } unset($shift); successResponse([ 'ongoing_shift' => $ongoingShift, 'today_shifts' => $todayShifts, 'date' => $today, 'leave_balance' => $leaveBalance, 'monthly_fund' => $monthlyFund, 'project_logs' => $projectLogs, 'active_project_id' => $activeProjectId, ]); } function handleGetHistory(PDO $pdo, int $userId): void { $month = validateMonth(); $year = (int)substr($month, 0, 4); $monthNum = (int)substr($month, 5, 2); $startDate = "{$month}-01"; $endDate = date('Y-m-t', strtotime($startDate)); $stmt = $pdo->prepare(' SELECT id, user_id, shift_date, arrival_time, arrival_address, break_start, break_end, departure_time, departure_address, notes, project_id, leave_type, leave_hours, created_at FROM attendance WHERE user_id = ? AND shift_date BETWEEN ? AND ? ORDER BY shift_date DESC '); $stmt->execute([$userId, $startDate, $endDate]); $records = $stmt->fetchAll(); enrichRecordsWithProjectLogs($pdo, $records); $totalMinutes = 0; $vacationHours = 0; $sickHours = 0; $holidayHours = 0; $unpaidHours = 0; foreach ($records as $record) { $leaveType = $record['leave_type'] ?? 'work'; $leaveHours = (float)($record['leave_hours'] ?? 0); if ($leaveType === 'vacation') { $vacationHours += $leaveHours; } elseif ($leaveType === 'sick') { $sickHours += $leaveHours; } elseif ($leaveType === 'holiday') { $holidayHours += $leaveHours; } elseif ($leaveType === 'unpaid') { $unpaidHours += $leaveHours; } else { $totalMinutes += calculateWorkMinutes($record); } } $fund = CzechHolidays::getMonthlyWorkFund($year, $monthNum); $businessDays = CzechHolidays::getBusinessDaysInMonth($year, $monthNum); $workedHours = round($totalMinutes / 60, 1); $leaveHoursCovered = $vacationHours + $sickHours; $covered = $workedHours + $leaveHoursCovered; $remaining = max(0, round($fund - $covered, 1)); $overtime = max(0, round($covered - $fund, 1)); $leaveBalance = getLeaveBalance($pdo, $userId, $year); successResponse([ 'records' => $records, 'month' => $month, 'year' => $year, 'month_name' => getCzechMonthName($monthNum) . ' ' . $year, 'total_minutes' => $totalMinutes, 'vacation_hours' => $vacationHours, 'sick_hours' => $sickHours, 'holiday_hours' => $holidayHours, 'unpaid_hours' => $unpaidHours, 'leave_balance' => $leaveBalance, 'monthly_fund' => [ 'fund' => $fund, 'business_days' => $businessDays, 'worked' => $workedHours, 'leave_hours' => $leaveHoursCovered, 'covered' => $covered, 'remaining' => $remaining, 'overtime' => $overtime, ], ]); } function handlePunch(PDO $pdo, int $userId): void { $input = getJsonInput(); $action = $input['punch_action'] ?? ''; $today = date('Y-m-d'); $rawNow = date('Y-m-d H:i:s'); $lat = isset($input['latitude']) && $input['latitude'] !== '' ? (float)$input['latitude'] : null; $lng = isset($input['longitude']) && $input['longitude'] !== '' ? (float)$input['longitude'] : null; $accuracy = isset($input['accuracy']) && $input['accuracy'] !== '' ? (float)$input['accuracy'] : null; $address = !empty($input['address']) ? $input['address'] : null; $stmt = $pdo->prepare(" SELECT id, user_id, shift_date, arrival_time, break_start, break_end, departure_time, notes, project_id, leave_type, created_at FROM attendance WHERE user_id = ? AND departure_time IS NULL AND (leave_type IS NULL OR leave_type = 'work') ORDER BY created_at DESC LIMIT 1 "); $stmt->execute([$userId]); $ongoingShift = $stmt->fetch(); if ($action === 'arrival' && !$ongoingShift) { $now = roundUpTo15Minutes($rawNow); $stmt = $pdo->prepare(' INSERT INTO attendance (user_id, shift_date, arrival_time, arrival_lat, arrival_lng, arrival_accuracy, arrival_address) VALUES (?, ?, ?, ?, ?, ?, ?) '); $stmt->execute([$userId, $today, $now, $lat, $lng, $accuracy, $address]); AuditLog::logCreate('attendance', (int)$pdo->lastInsertId(), [ 'arrival_time' => $now, 'location' => $address, ], 'Příchod zaznamenán'); successResponse(null, 'Příchod zaznamenán'); } elseif ($ongoingShift) { switch ($action) { case 'break_start': if ($ongoingShift['arrival_time'] && !$ongoingShift['break_start']) { $breakStart = roundToNearest10Minutes($rawNow); $breakEnd = date('Y-m-d H:i:s', strtotime($breakStart) + (30 * 60)); $stmt = $pdo->prepare('UPDATE attendance SET break_start = ?, break_end = ? WHERE id = ?'); $stmt->execute([$breakStart, $breakEnd, $ongoingShift['id']]); successResponse(null, 'Pauza zaznamenána'); } else { errorResponse('Nelze zadat pauzu'); } break; case 'departure': if ($ongoingShift['arrival_time'] && !$ongoingShift['departure_time']) { $now = roundDownTo15Minutes($rawNow); // Auto-add break if shift is longer than 6h and no break if (!$ongoingShift['break_start'] && !$ongoingShift['break_end']) { $arrivalTime = strtotime($ongoingShift['arrival_time']); $departureTime = strtotime($now); $hoursWorked = ($departureTime - $arrivalTime) / 3600; if ($hoursWorked > 12) { $midPoint = $arrivalTime + (($departureTime - $arrivalTime) / 2); $breakStart = roundToNearest10Minutes(date('Y-m-d H:i:s', $midPoint - (30 * 60))); $breakEnd = roundToNearest10Minutes(date('Y-m-d H:i:s', $midPoint + (30 * 60))); $stmt = $pdo->prepare('UPDATE attendance SET break_start = ?, break_end = ? WHERE id = ?'); $stmt->execute([$breakStart, $breakEnd, $ongoingShift['id']]); } elseif ($hoursWorked > 6) { $midPoint = $arrivalTime + (($departureTime - $arrivalTime) / 2); $breakStart = roundToNearest10Minutes(date('Y-m-d H:i:s', $midPoint - (15 * 60))); $breakEnd = roundToNearest10Minutes(date('Y-m-d H:i:s', $midPoint + (15 * 60))); $stmt = $pdo->prepare('UPDATE attendance SET break_start = ?, break_end = ? WHERE id = ?'); $stmt->execute([$breakStart, $breakEnd, $ongoingShift['id']]); } } $stmt = $pdo->prepare(' UPDATE attendance SET departure_time = ?, departure_lat = ?, departure_lng = ?, departure_accuracy = ?, departure_address = ? WHERE id = ? '); $stmt->execute([$now, $lat, $lng, $accuracy, $address, $ongoingShift['id']]); // Close any open project log $stmt = $pdo->prepare(' UPDATE attendance_project_logs SET ended_at = ? WHERE attendance_id = ? AND ended_at IS NULL '); $stmt->execute([$now, $ongoingShift['id']]); AuditLog::logUpdate('attendance', $ongoingShift['id'], [], [ 'departure_time' => $now, 'location' => $address, ], 'Odchod zaznamenán'); successResponse(null, 'Odchod zaznamenán'); } else { errorResponse('Nelze zadat odchod'); } break; default: errorResponse('Neplatná akce'); } } else { errorResponse('Neplatná akce - nemáte aktivní směnu'); } } function handleUpdateAddress(PDO $pdo, int $userId): void { $input = getJsonInput(); $address = trim($input['address'] ?? ''); $punchAction = $input['punch_action'] ?? ''; if (!$address) { successResponse(null); return; } if ($punchAction === 'arrival') { $stmt = $pdo->prepare(" UPDATE attendance SET arrival_address = ? WHERE id = ( SELECT id FROM ( SELECT id FROM attendance WHERE user_id = ? AND (arrival_address IS NULL OR arrival_address = '') ORDER BY created_at DESC LIMIT 1 ) t ) "); } else { $stmt = $pdo->prepare(" UPDATE attendance SET departure_address = ? WHERE id = ( SELECT id FROM ( SELECT id FROM attendance WHERE user_id = ? AND (departure_address IS NULL OR departure_address = '') AND departure_time IS NOT NULL ORDER BY created_at DESC LIMIT 1 ) t ) "); } $stmt->execute([$address, $userId]); successResponse(null); } function handleAddLeave(PDO $pdo, int $userId): void { $input = getJsonInput(); $leaveType = $input['leave_type'] ?? ''; $leaveDate = $input['leave_date'] ?? ''; $leaveHours = (float)($input['leave_hours'] ?? 8); $notes = trim($input['notes'] ?? ''); if (!$leaveType || !$leaveDate || $leaveHours <= 0) { errorResponse('Vyplňte všechna povinná pole'); } if (!in_array($leaveType, ['vacation', 'sick', 'unpaid'])) { errorResponse('Neplatný typ nepřítomnosti'); } if ($leaveType === 'vacation') { $year = (int)date('Y', strtotime($leaveDate)); $balance = getLeaveBalance($pdo, $userId, $year); if ($balance['vacation_remaining'] < $leaveHours) { errorResponse( "Nemáte dostatek hodin dovolené. Zbývá vám " . "{$balance['vacation_remaining']} hodin, požadujete {$leaveHours} hodin." ); } } $stmt = $pdo->prepare(' INSERT INTO attendance (user_id, shift_date, leave_type, leave_hours, notes) VALUES (?, ?, ?, ?, ?) '); $stmt->execute([$userId, $leaveDate, $leaveType, $leaveHours, $notes ?: null]); updateLeaveBalance($pdo, $userId, $leaveDate, $leaveType, $leaveHours); AuditLog::logCreate('attendance', (int)$pdo->lastInsertId(), [ 'leave_type' => $leaveType, 'leave_hours' => $leaveHours, ], "Zaznamenána nepřítomnost: $leaveType"); successResponse(null, 'Nepřítomnost byla zaznamenána'); } function handleSaveNotes(PDO $pdo, int $userId): void { $input = getJsonInput(); $notes = trim($input['notes'] ?? ''); $stmt = $pdo->prepare(' SELECT id FROM attendance WHERE user_id = ? AND departure_time IS NULL ORDER BY created_at DESC LIMIT 1 '); $stmt->execute([$userId]); $currentShift = $stmt->fetch(); if (!$currentShift) { errorResponse('Nemáte aktivní směnu'); } $stmt = $pdo->prepare('UPDATE attendance SET notes = ? WHERE id = ?'); $stmt->execute([$notes, $currentShift['id']]); successResponse(null, 'Poznámka byla uložena'); } function handleGetProjects(): void { try { $pdo = db(); $stmt = $pdo->query( "SELECT id, project_number, name FROM projects WHERE status = 'aktivni' ORDER BY project_number ASC" ); $projects = $stmt->fetchAll(); successResponse(['projects' => $projects]); } catch (\Exception $e) { error_log('Failed to fetch projects: ' . $e->getMessage()); successResponse(['projects' => []]); } } function handleSwitchProject(PDO $pdo, int $userId): void { $input = getJsonInput(); /** @var mixed $rawProjectId */ $rawProjectId = $input['project_id'] ?? null; $projectId = isset($input['project_id']) && $rawProjectId !== '' && $rawProjectId !== null ? (int)$rawProjectId : null; $stmt = $pdo->prepare(" SELECT id FROM attendance WHERE user_id = ? AND departure_time IS NULL AND (leave_type IS NULL OR leave_type = 'work') ORDER BY created_at DESC LIMIT 1 "); $stmt->execute([$userId]); $currentShift = $stmt->fetch(); if (!$currentShift) { errorResponse('Nemáte aktivní směnu'); } $attendanceId = $currentShift['id']; $now = date('Y-m-d H:i:s'); $stmt = $pdo->prepare( 'UPDATE attendance_project_logs SET ended_at = ? WHERE attendance_id = ? AND ended_at IS NULL' ); $stmt->execute([$now, $attendanceId]); if ($projectId) { $stmt = $pdo->prepare( 'INSERT INTO attendance_project_logs (attendance_id, project_id, started_at) VALUES (?, ?, ?)' ); $stmt->execute([$attendanceId, $projectId, $now]); } $stmt = $pdo->prepare('UPDATE attendance SET project_id = ? WHERE id = ?'); $stmt->execute([$projectId, $attendanceId]); successResponse(null, $projectId ? 'Projekt přepnut' : 'Projekt zastaven'); } /** @param array $authData */ function handleGetProjectLogs(PDO $pdo, int $currentUserId, array $authData): void { $attendanceId = (int)($_GET['attendance_id'] ?? 0); if (!$attendanceId) { errorResponse('attendance_id je povinné'); } // Ověření vlastnictví záznamu nebo admin oprávnění if (!hasPermission($authData, 'attendance.admin')) { $ownerStmt = $pdo->prepare('SELECT user_id FROM attendance WHERE id = ?'); $ownerStmt->execute([$attendanceId]); $owner = $ownerStmt->fetch(); if (!$owner || (int)$owner['user_id'] !== $currentUserId) { errorResponse('Nemáte oprávnění zobrazit tyto záznamy', 403); } } $stmt = $pdo->prepare( 'SELECT id, attendance_id, project_id, started_at, ended_at, hours, minutes FROM attendance_project_logs WHERE attendance_id = ? ORDER BY started_at ASC' ); $stmt->execute([$attendanceId]); $logs = $stmt->fetchAll(); $projectIds = []; foreach ($logs as $l) { $projectIds[$l['project_id']] = $l['project_id']; } $projNameMap = fetchProjectNames($projectIds); foreach ($logs as &$l) { $l['project_name'] = $projNameMap[$l['project_id']] ?? null; } unset($l); successResponse(['logs' => $logs]); } function handleSaveProjectLogs(PDO $pdo): void { $input = getJsonInput(); $attendanceId = (int)($input['attendance_id'] ?? 0); $logs = $input['project_logs'] ?? []; if (!$attendanceId) { errorResponse('attendance_id je povinné'); } $stmt = $pdo->prepare('SELECT id FROM attendance WHERE id = ?'); $stmt->execute([$attendanceId]); $record = $stmt->fetch(); if (!$record) { errorResponse('Záznam nebyl nalezen', 404); } $stmt = $pdo->prepare('DELETE FROM attendance_project_logs WHERE attendance_id = ?'); $stmt->execute([$attendanceId]); if (!empty($logs)) { $stmt = $pdo->prepare( 'INSERT INTO attendance_project_logs (attendance_id, project_id, hours, minutes) VALUES (?, ?, ?, ?)' ); foreach ($logs as $log) { $projectId = (int)($log['project_id'] ?? 0); if (!$projectId) { continue; } $h = (int)($log['hours'] ?? 0); $m = (int)($log['minutes'] ?? 0); if ($h === 0 && $m === 0) { continue; } $stmt->execute([$attendanceId, $projectId, $h, $m]); } } successResponse(null, 'Projektové záznamy byly uloženy'); }