Files
app/api/includes/AttendanceHelpers.php
Simon 5529219234 refactor: sjednoceni zdroje casu na MySQL NOW() + audit log cleanup a UI
- attendance handlery pouzivaji getDbNow() misto PHP date()
- nova helper funkce getDbNow() v AttendanceHelpers.php
- audit log: cleanup endpoint (POST) s volbou stari zaznamu
- audit log: filtry na jednom radku
- dashboard: aktivita prejmenovana na Audit log s odkazem

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

387 lines
12 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);
/**
* Vraci aktualni cas a datum z MySQL (jednotny zdroj casu)
* @return array{now: string, today: string, year: int, month: int}
*/
function getDbNow(PDO $pdo): array
{
$row = $pdo->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<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);
$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<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 [];
}
}