Files
app/api/includes/AttendanceHelpers.php
Simon 758be819c3 feat: P4 backend kvalita - SELECT * fix, overdue konsolidace, Validator
- SELECT * nahrazen explicitnimi sloupci ve 22 PHP souborech (69+ vyskytu)
- users-handlers.php: password_hash explicitne vyloucen z dotazu
- Overdue detekce presunuta do invoices.php routeru (1x pred dispatch misto 3x v handlerech)
- Validator.php: validacni helper s pravidly required, string, int, email, in, numeric
- PaginationHelper: PHPStan typy opraveny

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 18:42:42 +01:00

373 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Attendance helper functions - shared between user and admin handlers
*/
declare(strict_types=1);
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<string, mixed> $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<int, int> $userIds
* @return array<int, array{vacation_total: float, vacation_used: float, vacation_remaining: float, sick_used: float}>
*/
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<int, array<string, mixed>> $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<array<string, mixed>> $records
* @return array<int, array<string, mixed>>
*/
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<array<string, mixed>> $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);
$today = date('Y-m-d');
$stmt = $pdo->prepare("
SELECT DISTINCT user_id FROM attendance
WHERE shift_date = ?
AND arrival_time IS NOT NULL
AND departure_time IS NULL
AND (leave_type IS NULL OR leave_type = 'work')
");
$stmt->execute([$today]);
$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<int, int> $projectIds
* @return array<int, string>
*/
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 [];
}
}