Initial commit
This commit is contained in:
968
api/includes/AttendanceAdmin.php
Normal file
968
api/includes/AttendanceAdmin.php
Normal file
@@ -0,0 +1,968 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Attendance admin handler functions
|
||||
* Requires: AttendanceHelpers.php, CzechHolidays.php, AuditLog.php
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
function handleGetAdmin(PDO $pdo): void
|
||||
{
|
||||
$month = validateMonth();
|
||||
$filterUserId = isset($_GET['user_id']) ? (int)$_GET['user_id'] : null;
|
||||
|
||||
$year = (int)substr($month, 0, 4);
|
||||
$startDate = "{$month}-01";
|
||||
$endDate = date('Y-m-t', strtotime($startDate));
|
||||
|
||||
$sql = "
|
||||
SELECT a.*, CONCAT(u.first_name, ' ', u.last_name) as user_name
|
||||
FROM attendance a
|
||||
JOIN users u ON a.user_id = u.id
|
||||
WHERE a.shift_date BETWEEN ? AND ?
|
||||
";
|
||||
$params = [$startDate, $endDate];
|
||||
|
||||
if ($filterUserId) {
|
||||
$sql .= ' AND a.user_id = ?';
|
||||
$params[] = $filterUserId;
|
||||
}
|
||||
|
||||
$sql .= ' ORDER BY a.shift_date DESC, a.arrival_time DESC';
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$records = $stmt->fetchAll();
|
||||
|
||||
enrichRecordsWithProjectLogs($pdo, $records);
|
||||
|
||||
$stmt = $pdo->query(
|
||||
"SELECT id, CONCAT(first_name, ' ', last_name) as name
|
||||
FROM users WHERE is_active = 1 ORDER BY last_name"
|
||||
);
|
||||
$users = $stmt->fetchAll();
|
||||
|
||||
$userTotals = calculateUserTotals($records);
|
||||
$leaveBalances = getLeaveBalancesBatch(
|
||||
$pdo,
|
||||
array_keys($userTotals),
|
||||
$year
|
||||
);
|
||||
|
||||
$monthNum = (int)substr($month, 5, 2);
|
||||
addFundDataToUserTotals($pdo, $userTotals, $year, $monthNum);
|
||||
|
||||
successResponse([
|
||||
'records' => $records,
|
||||
'users' => $users,
|
||||
'month' => $month,
|
||||
'user_totals' => $userTotals,
|
||||
'leave_balances' => $leaveBalances,
|
||||
]);
|
||||
}
|
||||
|
||||
function handleGetBalances(PDO $pdo): void
|
||||
{
|
||||
$year = (int)($_GET['year'] ?? date('Y'));
|
||||
|
||||
$stmt = $pdo->query(
|
||||
"SELECT id, CONCAT(first_name, ' ', last_name) as name
|
||||
FROM users WHERE is_active = 1 ORDER BY last_name"
|
||||
);
|
||||
$users = $stmt->fetchAll();
|
||||
|
||||
$userIds = array_column($users, 'id');
|
||||
$batchBalances = getLeaveBalancesBatch($pdo, $userIds, $year);
|
||||
|
||||
$balances = [];
|
||||
foreach ($users as $user) {
|
||||
$balances[$user['id']] = array_merge(
|
||||
['name' => $user['name']],
|
||||
$batchBalances[$user['id']]
|
||||
);
|
||||
}
|
||||
|
||||
successResponse([
|
||||
'users' => $users,
|
||||
'balances' => $balances,
|
||||
'year' => $year,
|
||||
]);
|
||||
}
|
||||
|
||||
function handleGetWorkFund(PDO $pdo): void
|
||||
{
|
||||
$year = (int)($_GET['year'] ?? date('Y'));
|
||||
$currentYear = (int)date('Y');
|
||||
$currentMonth = (int)date('m');
|
||||
|
||||
$maxMonth = ($year < $currentYear) ? 12 : (($year === $currentYear) ? $currentMonth : 0);
|
||||
|
||||
if ($maxMonth === 0) {
|
||||
successResponse(['months' => [], 'holidays' => [], 'year' => $year]);
|
||||
return;
|
||||
}
|
||||
|
||||
$stmt = $pdo->query(
|
||||
"SELECT id, CONCAT(first_name, ' ', last_name) as name
|
||||
FROM users WHERE is_active = 1 ORDER BY last_name"
|
||||
);
|
||||
$users = $stmt->fetchAll();
|
||||
|
||||
$startDate = sprintf('%04d-01-01', $year);
|
||||
$endDate = sprintf('%04d-%02d-%02d', $year, $maxMonth, cal_days_in_month(CAL_GREGORIAN, $maxMonth, $year));
|
||||
|
||||
$stmt = $pdo->prepare('SELECT * FROM attendance WHERE shift_date BETWEEN ? AND ? ORDER BY shift_date');
|
||||
$stmt->execute([$startDate, $endDate]);
|
||||
$allRecords = $stmt->fetchAll();
|
||||
|
||||
$monthUserData = [];
|
||||
foreach ($allRecords as $rec) {
|
||||
$m = (int)date('m', strtotime($rec['shift_date']));
|
||||
$uid = $rec['user_id'];
|
||||
if (!isset($monthUserData[$m][$uid])) {
|
||||
$monthUserData[$m][$uid] = ['minutes' => 0, 'vacation' => 0, 'sick' => 0, 'holiday' => 0, 'unpaid' => 0];
|
||||
}
|
||||
$lt = $rec['leave_type'] ?? 'work';
|
||||
$lh = (float)($rec['leave_hours'] ?? 0);
|
||||
if ($lt === 'work') {
|
||||
if ($rec['departure_time']) {
|
||||
$monthUserData[$m][$uid]['minutes'] += calculateWorkMinutes($rec);
|
||||
}
|
||||
} elseif ($lt === 'vacation') {
|
||||
$monthUserData[$m][$uid]['vacation'] += $lh;
|
||||
} elseif ($lt === 'sick') {
|
||||
$monthUserData[$m][$uid]['sick'] += $lh;
|
||||
} elseif ($lt === 'holiday') {
|
||||
$monthUserData[$m][$uid]['holiday'] += $lh;
|
||||
} elseif ($lt === 'unpaid') {
|
||||
$monthUserData[$m][$uid]['unpaid'] += $lh;
|
||||
}
|
||||
}
|
||||
|
||||
$months = [];
|
||||
for ($m = 1; $m <= $maxMonth; $m++) {
|
||||
$fund = CzechHolidays::getMonthlyWorkFund($year, $m);
|
||||
$businessDays = CzechHolidays::getBusinessDaysInMonth($year, $m);
|
||||
$monthName = getCzechMonthName($m);
|
||||
|
||||
$userStats = [];
|
||||
foreach ($users as $user) {
|
||||
$uid = $user['id'];
|
||||
$ud = $monthUserData[$m][$uid] ?? [
|
||||
'minutes' => 0, 'vacation' => 0, 'sick' => 0,
|
||||
'holiday' => 0, 'unpaid' => 0,
|
||||
];
|
||||
$worked = round($ud['minutes'] / 60, 1);
|
||||
$leave = $ud['vacation'] + $ud['sick'];
|
||||
$covered = $worked + $leave;
|
||||
$missing = max(0, round($fund - $covered, 1));
|
||||
$overtime = max(0, round($covered - $fund, 1));
|
||||
|
||||
$userStats[$uid] = [
|
||||
'name' => $user['name'],
|
||||
'worked' => $worked,
|
||||
'vacation' => $ud['vacation'],
|
||||
'sick' => $ud['sick'],
|
||||
'holiday' => $ud['holiday'],
|
||||
'unpaid' => $ud['unpaid'],
|
||||
'leave' => $leave,
|
||||
'covered' => $covered,
|
||||
'missing' => $missing,
|
||||
'overtime' => $overtime,
|
||||
];
|
||||
}
|
||||
|
||||
$months[$m] = [
|
||||
'month' => $m,
|
||||
'month_name' => $monthName,
|
||||
'fund' => $fund,
|
||||
'business_days' => $businessDays,
|
||||
'users' => $userStats,
|
||||
];
|
||||
}
|
||||
|
||||
$userIds = array_column($users, 'id');
|
||||
$batchBalances = getLeaveBalancesBatch($pdo, $userIds, $year);
|
||||
$balances = [];
|
||||
foreach ($users as $user) {
|
||||
$balances[$user['id']] = array_merge(
|
||||
['name' => $user['name']],
|
||||
$batchBalances[$user['id']]
|
||||
);
|
||||
}
|
||||
|
||||
$holidays = CzechHolidays::getHolidays($year);
|
||||
|
||||
successResponse([
|
||||
'months' => $months,
|
||||
'balances' => $balances,
|
||||
'holidays' => $holidays,
|
||||
'users' => $users,
|
||||
'year' => $year,
|
||||
]);
|
||||
}
|
||||
|
||||
function handleGetLocation(PDO $pdo, int $recordId): void
|
||||
{
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT a.*, CONCAT(u.first_name, ' ', u.last_name) as user_name
|
||||
FROM attendance a
|
||||
JOIN users u ON a.user_id = u.id
|
||||
WHERE a.id = ?
|
||||
");
|
||||
$stmt->execute([$recordId]);
|
||||
$record = $stmt->fetch();
|
||||
|
||||
if (!$record) {
|
||||
errorResponse('Záznam nebyl nalezen', 404);
|
||||
}
|
||||
|
||||
successResponse(['record' => $record]);
|
||||
}
|
||||
|
||||
function handleGetUsers(PDO $pdo): void
|
||||
{
|
||||
$stmt = $pdo->query(
|
||||
"SELECT id, CONCAT(first_name, ' ', last_name) as name
|
||||
FROM users WHERE is_active = 1 ORDER BY last_name"
|
||||
);
|
||||
$users = $stmt->fetchAll();
|
||||
successResponse(['users' => $users]);
|
||||
}
|
||||
|
||||
function handleCreateAttendance(PDO $pdo): void
|
||||
{
|
||||
$input = getJsonInput();
|
||||
|
||||
$userId = (int)($input['user_id'] ?? 0);
|
||||
$shiftDate = $input['shift_date'] ?? '';
|
||||
$leaveType = $input['leave_type'] ?? 'work';
|
||||
$leaveHours = $input['leave_hours'] ?? null;
|
||||
$notes = $input['notes'] ?? null;
|
||||
|
||||
if (!$userId || !$shiftDate) {
|
||||
errorResponse('Vyplňte zaměstnance a datum směny');
|
||||
}
|
||||
|
||||
if ($leaveType !== 'work') {
|
||||
$leaveHours = $leaveHours ?: 8;
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
$stmt = $pdo->prepare('
|
||||
INSERT INTO attendance (user_id, shift_date, leave_type, leave_hours, notes)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
');
|
||||
$stmt->execute([$userId, $shiftDate, $leaveType, $leaveHours, $notes]);
|
||||
|
||||
updateLeaveBalance($pdo, $userId, $shiftDate, $leaveType, (float)$leaveHours);
|
||||
$pdo->commit();
|
||||
} catch (\Throwable $e) {
|
||||
$pdo->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
} else {
|
||||
$arrivalDate = $input['arrival_date'] ?? $shiftDate;
|
||||
$arrivalTime = $input['arrival_time'] ?? null;
|
||||
$breakStartDate = $input['break_start_date'] ?? null;
|
||||
$breakStartTime = $input['break_start_time'] ?? null;
|
||||
$breakEndDate = $input['break_end_date'] ?? null;
|
||||
$breakEndTime = $input['break_end_time'] ?? null;
|
||||
$departureDate = $input['departure_date'] ?? null;
|
||||
$departureTime = $input['departure_time'] ?? null;
|
||||
/** @var mixed $rawProjectId */
|
||||
$rawProjectId = $input['project_id'] ?? null;
|
||||
$projectId = isset($input['project_id']) && $rawProjectId !== '' && $rawProjectId !== null
|
||||
? (int)$rawProjectId
|
||||
: null;
|
||||
|
||||
$arrival = $arrivalTime ? "{$arrivalDate} {$arrivalTime}:00" : null;
|
||||
$breakStart = ($breakStartDate && $breakStartTime) ? "{$breakStartDate} {$breakStartTime}:00" : null;
|
||||
$breakEnd = ($breakEndDate && $breakEndTime) ? "{$breakEndDate} {$breakEndTime}:00" : null;
|
||||
$departure = ($departureDate && $departureTime) ? "{$departureDate} {$departureTime}:00" : null;
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO attendance
|
||||
(user_id, shift_date, arrival_time, break_start,
|
||||
break_end, departure_time, leave_type, notes, project_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'work', ?, ?)
|
||||
");
|
||||
$stmt->execute([$userId, $shiftDate, $arrival, $breakStart, $breakEnd, $departure, $notes, $projectId]);
|
||||
}
|
||||
|
||||
$newId = (int)$pdo->lastInsertId();
|
||||
|
||||
$projectLogs = $input['project_logs'] ?? [];
|
||||
if (!empty($projectLogs) && $leaveType === 'work') {
|
||||
$logStmt = $pdo->prepare(
|
||||
'INSERT INTO attendance_project_logs
|
||||
(attendance_id, project_id, hours, minutes)
|
||||
VALUES (?, ?, ?, ?)'
|
||||
);
|
||||
foreach ($projectLogs as $log) {
|
||||
$pid = (int)($log['project_id'] ?? 0);
|
||||
if (!$pid) {
|
||||
continue;
|
||||
}
|
||||
$h = (int)($log['hours'] ?? 0);
|
||||
$m = (int)($log['minutes'] ?? 0);
|
||||
if ($h === 0 && $m === 0) {
|
||||
continue;
|
||||
}
|
||||
$logStmt->execute([$newId, $pid, $h, $m]);
|
||||
}
|
||||
}
|
||||
|
||||
AuditLog::logCreate('attendance', $newId, $input, 'Admin vytvořil záznam docházky');
|
||||
|
||||
successResponse(['id' => $newId], 'Záznam byl vytvořen');
|
||||
}
|
||||
|
||||
function handleBulkAttendance(PDO $pdo): void
|
||||
{
|
||||
$input = getJsonInput();
|
||||
|
||||
$monthStr = $input['month'] ?? '';
|
||||
$userIds = $input['user_ids'] ?? [];
|
||||
$arrivalTime = trim($input['arrival_time'] ?? '08:00');
|
||||
$departureTime = trim($input['departure_time'] ?? '16:30');
|
||||
$breakStartTime = trim($input['break_start_time'] ?? '12:00');
|
||||
$breakEndTime = trim($input['break_end_time'] ?? '12:30');
|
||||
|
||||
if (!$monthStr || !preg_match('/^\d{4}-\d{2}$/', $monthStr)) {
|
||||
errorResponse('Měsíc je povinný (formát YYYY-MM)');
|
||||
}
|
||||
|
||||
if (empty($userIds) || !is_array($userIds)) {
|
||||
errorResponse('Vyberte alespoň jednoho zaměstnance');
|
||||
}
|
||||
|
||||
$year = (int)substr($monthStr, 0, 4);
|
||||
$month = (int)substr($monthStr, 5, 2);
|
||||
|
||||
$holidays = CzechHolidays::getHolidays($year);
|
||||
$daysInMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year);
|
||||
|
||||
$inserted = 0;
|
||||
$skipped = 0;
|
||||
|
||||
// Batch fetch existing records (eliminates N*M queries)
|
||||
$dateFrom = sprintf('%04d-%02d-01', $year, $month);
|
||||
$dateTo = sprintf('%04d-%02d-%02d', $year, $month, $daysInMonth);
|
||||
$userIdInts = array_map('intval', $userIds);
|
||||
$placeholders = implode(',', array_fill(0, count($userIdInts), '?'));
|
||||
$existStmt = $pdo->prepare("
|
||||
SELECT user_id, shift_date FROM attendance
|
||||
WHERE user_id IN ($placeholders) AND shift_date BETWEEN ? AND ?
|
||||
");
|
||||
$existParams = array_merge($userIdInts, [$dateFrom, $dateTo]);
|
||||
$existStmt->execute($existParams);
|
||||
$existingRecords = [];
|
||||
foreach ($existStmt->fetchAll() as $row) {
|
||||
$existingRecords[$row['user_id'] . ':' . $row['shift_date']] = true;
|
||||
}
|
||||
|
||||
$holidayStmt = $pdo->prepare("
|
||||
INSERT INTO attendance (user_id, shift_date, leave_type, leave_hours, notes)
|
||||
VALUES (?, ?, 'holiday', 8, 'Státní svátek')
|
||||
");
|
||||
$workStmt = $pdo->prepare('
|
||||
INSERT INTO attendance (user_id, shift_date, arrival_time, departure_time, break_start, break_end)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
');
|
||||
|
||||
foreach ($userIdInts as $userId) {
|
||||
for ($day = 1; $day <= $daysInMonth; $day++) {
|
||||
$date = sprintf('%04d-%02d-%02d', $year, $month, $day);
|
||||
$dayOfWeek = (int)date('N', strtotime($date));
|
||||
|
||||
if ($dayOfWeek > 5) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$isHoliday = in_array($date, $holidays, true);
|
||||
|
||||
if (isset($existingRecords[$userId . ':' . $date])) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($isHoliday) {
|
||||
$holidayStmt->execute([$userId, $date]);
|
||||
} else {
|
||||
$arrival = $date . ' ' . $arrivalTime . ':00';
|
||||
$departure = $date . ' ' . $departureTime . ':00';
|
||||
$breakStart = $date . ' ' . $breakStartTime . ':00';
|
||||
$breakEnd = $date . ' ' . $breakEndTime . ':00';
|
||||
$workStmt->execute([$userId, $date, $arrival, $departure, $breakStart, $breakEnd]);
|
||||
}
|
||||
$inserted++;
|
||||
}
|
||||
}
|
||||
|
||||
AuditLog::logCreate('attendance', 0, [
|
||||
'month' => $monthStr,
|
||||
'user_ids' => $userIds,
|
||||
'inserted' => $inserted,
|
||||
'skipped' => $skipped,
|
||||
], "Admin hromadně vytvořil $inserted záznamů docházky pro měsíc $monthStr");
|
||||
|
||||
$msg = "Vytvořeno $inserted záznamů";
|
||||
if ($skipped > 0) {
|
||||
$msg .= " ($skipped přeskočeno — již existují)";
|
||||
}
|
||||
|
||||
successResponse(['inserted' => $inserted, 'skipped' => $skipped], $msg);
|
||||
}
|
||||
|
||||
function handleUpdateBalance(PDO $pdo): void
|
||||
{
|
||||
$input = getJsonInput();
|
||||
|
||||
$userId = (int)($input['user_id'] ?? 0);
|
||||
$year = (int)($input['year'] ?? date('Y'));
|
||||
$actionType = $input['action_type'] ?? 'edit';
|
||||
|
||||
if (!$userId) {
|
||||
errorResponse('ID uživatele je povinné');
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare('SELECT id FROM leave_balances WHERE user_id = ? AND year = ?');
|
||||
$stmt->execute([$userId, $year]);
|
||||
$exists = $stmt->fetch();
|
||||
|
||||
if ($actionType === 'reset') {
|
||||
if ($exists) {
|
||||
$stmt = $pdo->prepare(
|
||||
'UPDATE leave_balances
|
||||
SET vacation_used = 0, sick_used = 0
|
||||
WHERE user_id = ? AND year = ?'
|
||||
);
|
||||
$stmt->execute([$userId, $year]);
|
||||
}
|
||||
successResponse(null, 'Bilance byla resetována');
|
||||
} else {
|
||||
$vacationTotal = (float)($input['vacation_total'] ?? 160);
|
||||
$vacationUsed = (float)($input['vacation_used'] ?? 0);
|
||||
$sickUsed = (float)($input['sick_used'] ?? 0);
|
||||
|
||||
if ($exists) {
|
||||
$stmt = $pdo->prepare(
|
||||
'UPDATE leave_balances
|
||||
SET vacation_total = ?, vacation_used = ?, sick_used = ?
|
||||
WHERE user_id = ? AND year = ?'
|
||||
);
|
||||
$stmt->execute([$vacationTotal, $vacationUsed, $sickUsed, $userId, $year]);
|
||||
} else {
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO leave_balances
|
||||
(user_id, year, vacation_total, vacation_used, sick_used)
|
||||
VALUES (?, ?, ?, ?, ?)'
|
||||
);
|
||||
$stmt->execute([$userId, $year, $vacationTotal, $vacationUsed, $sickUsed]);
|
||||
}
|
||||
successResponse(null, 'Bilance byla aktualizována');
|
||||
}
|
||||
}
|
||||
|
||||
function handleUpdateAttendance(PDO $pdo, int $recordId): void
|
||||
{
|
||||
$stmt = $pdo->prepare('SELECT * FROM attendance WHERE id = ?');
|
||||
$stmt->execute([$recordId]);
|
||||
$record = $stmt->fetch();
|
||||
|
||||
if (!$record) {
|
||||
errorResponse('Záznam nebyl nalezen', 404);
|
||||
}
|
||||
|
||||
$input = getJsonInput();
|
||||
|
||||
$shiftDate = $input['shift_date'] ?? $record['shift_date'];
|
||||
$leaveType = $input['leave_type'] ?? 'work';
|
||||
$leaveHours = $input['leave_hours'] ?? null;
|
||||
$notes = $input['notes'] ?? null;
|
||||
|
||||
$oldLeaveType = $record['leave_type'] ?? 'work';
|
||||
$oldLeaveHours = $record['leave_hours'] ?? 0;
|
||||
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
if ($leaveType !== 'work') {
|
||||
$leaveHours = $leaveHours ?: 8;
|
||||
$stmt = $pdo->prepare('
|
||||
UPDATE attendance
|
||||
SET shift_date = ?, leave_type = ?, leave_hours = ?,
|
||||
arrival_time = NULL, break_start = NULL,
|
||||
break_end = NULL, departure_time = NULL, notes = ?
|
||||
WHERE id = ?
|
||||
');
|
||||
$stmt->execute([$shiftDate, $leaveType, $leaveHours, $notes, $recordId]);
|
||||
|
||||
if ($oldLeaveType !== 'work' && $oldLeaveHours > 0) {
|
||||
updateLeaveBalance(
|
||||
$pdo,
|
||||
(int)$record['user_id'],
|
||||
$record['shift_date'],
|
||||
$oldLeaveType,
|
||||
-(float)$oldLeaveHours
|
||||
);
|
||||
}
|
||||
updateLeaveBalance(
|
||||
$pdo,
|
||||
(int)$record['user_id'],
|
||||
$shiftDate,
|
||||
$leaveType,
|
||||
(float)$leaveHours
|
||||
);
|
||||
} else {
|
||||
$arrivalDate = $input['arrival_date'] ?? $shiftDate;
|
||||
$arrivalTime = $input['arrival_time'] ?? null;
|
||||
$breakStartDate = $input['break_start_date'] ?? null;
|
||||
$breakStartTime = $input['break_start_time'] ?? null;
|
||||
$breakEndDate = $input['break_end_date'] ?? null;
|
||||
$breakEndTime = $input['break_end_time'] ?? null;
|
||||
$departureDate = $input['departure_date'] ?? null;
|
||||
$departureTime = $input['departure_time'] ?? null;
|
||||
/** @var mixed $rawProjectId */
|
||||
$rawProjectId = $input['project_id'] ?? null;
|
||||
$projectId = isset($input['project_id']) && $rawProjectId !== '' && $rawProjectId !== null
|
||||
? (int)$rawProjectId
|
||||
: null;
|
||||
|
||||
$arrival = $arrivalTime ? "{$arrivalDate} {$arrivalTime}:00" : null;
|
||||
$breakStart = $breakStartTime ? "{$breakStartDate} {$breakStartTime}:00" : null;
|
||||
$breakEnd = $breakEndTime ? "{$breakEndDate} {$breakEndTime}:00" : null;
|
||||
$departure = $departureTime ? "{$departureDate} {$departureTime}:00" : null;
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE attendance
|
||||
SET shift_date = ?, arrival_time = ?, break_start = ?,
|
||||
break_end = ?, departure_time = ?,
|
||||
leave_type = 'work', leave_hours = NULL,
|
||||
notes = ?, project_id = ?
|
||||
WHERE id = ?
|
||||
");
|
||||
$stmt->execute([$shiftDate, $arrival, $breakStart, $breakEnd, $departure, $notes, $projectId, $recordId]);
|
||||
|
||||
if ($oldLeaveType !== 'work' && $oldLeaveHours > 0) {
|
||||
updateLeaveBalance(
|
||||
$pdo,
|
||||
(int)$record['user_id'],
|
||||
$record['shift_date'],
|
||||
$oldLeaveType,
|
||||
-(float)$oldLeaveHours
|
||||
);
|
||||
}
|
||||
}
|
||||
$pdo->commit();
|
||||
} catch (\Throwable $e) {
|
||||
$pdo->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$projectLogs = $input['project_logs'] ?? null;
|
||||
if ($projectLogs !== null) {
|
||||
$stmt = $pdo->prepare('DELETE FROM attendance_project_logs WHERE attendance_id = ?');
|
||||
$stmt->execute([$recordId]);
|
||||
|
||||
if (!empty($projectLogs) && ($input['leave_type'] ?? 'work') === 'work') {
|
||||
$logStmt = $pdo->prepare(
|
||||
'INSERT INTO attendance_project_logs
|
||||
(attendance_id, project_id, hours, minutes)
|
||||
VALUES (?, ?, ?, ?)'
|
||||
);
|
||||
foreach ($projectLogs as $log) {
|
||||
$pid = (int)($log['project_id'] ?? 0);
|
||||
if (!$pid) {
|
||||
continue;
|
||||
}
|
||||
$h = (int)($log['hours'] ?? 0);
|
||||
$m = (int)($log['minutes'] ?? 0);
|
||||
if ($h === 0 && $m === 0) {
|
||||
continue;
|
||||
}
|
||||
$logStmt->execute([$recordId, $pid, $h, $m]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AuditLog::logUpdate('attendance', $recordId, $record, $input, 'Admin upravil záznam docházky');
|
||||
|
||||
successResponse(null, 'Záznam byl aktualizován');
|
||||
}
|
||||
|
||||
function handleDeleteAttendance(PDO $pdo, int $recordId): void
|
||||
{
|
||||
$stmt = $pdo->prepare('SELECT * FROM attendance WHERE id = ?');
|
||||
$stmt->execute([$recordId]);
|
||||
$record = $stmt->fetch();
|
||||
|
||||
if (!$record) {
|
||||
errorResponse('Záznam nebyl nalezen', 404);
|
||||
}
|
||||
|
||||
$leaveType = $record['leave_type'] ?? 'work';
|
||||
$leaveHours = $record['leave_hours'] ?? 0;
|
||||
if ($leaveType !== 'work' && $leaveHours > 0) {
|
||||
updateLeaveBalance($pdo, (int)$record['user_id'], $record['shift_date'], $leaveType, -(float)$leaveHours);
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare('DELETE FROM attendance_project_logs WHERE attendance_id = ?');
|
||||
$stmt->execute([$recordId]);
|
||||
|
||||
$stmt = $pdo->prepare('DELETE FROM attendance WHERE id = ?');
|
||||
$stmt->execute([$recordId]);
|
||||
|
||||
AuditLog::logDelete('attendance', $recordId, $record, 'Admin smazal záznam docházky');
|
||||
|
||||
successResponse(null, 'Záznam byl smazán');
|
||||
}
|
||||
|
||||
function handleGetProjectReport(PDO $pdo): void
|
||||
{
|
||||
$yearParam = $_GET['year'] ?? null;
|
||||
$monthParam = $_GET['month'] ?? null;
|
||||
|
||||
if ($yearParam) {
|
||||
$yearInt = (int)$yearParam;
|
||||
$currentYear = (int)date('Y');
|
||||
$currentMonth = (int)date('m');
|
||||
$maxMonth = ($yearInt < $currentYear) ? 12 : (($yearInt === $currentYear) ? $currentMonth : 0);
|
||||
|
||||
if ($maxMonth === 0) {
|
||||
successResponse(['months' => [], 'year' => $yearInt]);
|
||||
return;
|
||||
}
|
||||
|
||||
$startDate = sprintf('%04d-01-01', $yearInt);
|
||||
$lastDay = cal_days_in_month(CAL_GREGORIAN, $maxMonth, $yearInt);
|
||||
$endDate = sprintf('%04d-%02d-%02d', $yearInt, $maxMonth, $lastDay);
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT a.user_id, a.id as attendance_id, a.shift_date,
|
||||
a.arrival_time, a.departure_time,
|
||||
a.break_start, a.break_end,
|
||||
CONCAT(u.first_name, ' ', u.last_name) as user_name
|
||||
FROM attendance a
|
||||
JOIN users u ON a.user_id = u.id
|
||||
WHERE a.shift_date BETWEEN ? AND ?
|
||||
AND a.departure_time IS NOT NULL
|
||||
AND (a.leave_type IS NULL OR a.leave_type = 'work')
|
||||
ORDER BY u.last_name ASC
|
||||
");
|
||||
$stmt->execute([$startDate, $endDate]);
|
||||
$workRecords = $stmt->fetchAll();
|
||||
|
||||
$totalWork = [];
|
||||
$attendanceIds = [];
|
||||
foreach ($workRecords as $rec) {
|
||||
$m = (int)date('m', strtotime($rec['shift_date']));
|
||||
$uid = $rec['user_id'];
|
||||
$attendanceIds[] = $rec['attendance_id'];
|
||||
if (!isset($totalWork[$m][$uid])) {
|
||||
$totalWork[$m][$uid] = ['name' => $rec['user_name'], 'minutes' => 0];
|
||||
}
|
||||
$totalWork[$m][$uid]['minutes'] += calculateWorkMinutes($rec);
|
||||
}
|
||||
|
||||
$loggedMinutes = [];
|
||||
$monthData = [];
|
||||
$projectIds = [];
|
||||
|
||||
if (!empty($attendanceIds)) {
|
||||
$placeholders = implode(',', array_fill(0, count($attendanceIds), '?'));
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT pl.project_id, pl.started_at, pl.ended_at, pl.hours, pl.minutes AS mins, a.user_id, a.shift_date
|
||||
FROM attendance_project_logs pl
|
||||
JOIN attendance a ON pl.attendance_id = a.id
|
||||
WHERE pl.attendance_id IN ($placeholders)
|
||||
AND (pl.hours IS NOT NULL OR pl.ended_at IS NOT NULL)
|
||||
");
|
||||
$stmt->execute($attendanceIds);
|
||||
$logs = $stmt->fetchAll();
|
||||
|
||||
foreach ($logs as $log) {
|
||||
$m = (int)date('m', strtotime($log['shift_date']));
|
||||
$pid = (int)$log['project_id'];
|
||||
$uid = $log['user_id'];
|
||||
$projectIds[$pid] = true;
|
||||
if ($log['hours'] !== null) {
|
||||
$minutes = (int)$log['hours'] * 60 + (int)$log['mins'];
|
||||
} else {
|
||||
$minutes = max(0, (strtotime($log['ended_at']) - strtotime($log['started_at'])) / 60);
|
||||
}
|
||||
|
||||
if (!isset($monthData[$m][$pid][$uid])) {
|
||||
$monthData[$m][$pid][$uid] = ['minutes' => 0];
|
||||
}
|
||||
$monthData[$m][$pid][$uid]['minutes'] += $minutes;
|
||||
|
||||
if (!isset($loggedMinutes[$m][$uid])) {
|
||||
$loggedMinutes[$m][$uid] = 0;
|
||||
}
|
||||
$loggedMinutes[$m][$uid] += $minutes;
|
||||
}
|
||||
}
|
||||
|
||||
// "Bez projektu" = total work - logged
|
||||
foreach ($totalWork as $m => $users) {
|
||||
foreach ($users as $uid => $ud) {
|
||||
$logged = $loggedMinutes[$m][$uid] ?? 0;
|
||||
$unlogged = $ud['minutes'] - $logged;
|
||||
if ($unlogged > 1) {
|
||||
if (!isset($monthData[$m][0][$uid])) {
|
||||
$monthData[$m][0][$uid] = ['minutes' => 0];
|
||||
}
|
||||
$monthData[$m][0][$uid]['minutes'] += $unlogged;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$projectMap = [];
|
||||
if (!empty($projectIds)) {
|
||||
try {
|
||||
$offersPdo = db();
|
||||
$ids = array_keys($projectIds);
|
||||
$placeholders = implode(',', array_fill(0, count($ids), '?'));
|
||||
$stmt2 = $offersPdo->prepare(
|
||||
"SELECT id, project_number, name
|
||||
FROM projects WHERE id IN ($placeholders)"
|
||||
);
|
||||
$stmt2->execute($ids);
|
||||
foreach ($stmt2->fetchAll() as $p) {
|
||||
$projectMap[$p['id']] = $p;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
error_log('Failed to fetch project names for yearly report: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$userNames = [];
|
||||
foreach ($totalWork as $m => $users) {
|
||||
foreach ($users as $uid => $ud) {
|
||||
$userNames[$uid] = $ud['name'];
|
||||
}
|
||||
}
|
||||
|
||||
$months = [];
|
||||
for ($m = 1; $m <= $maxMonth; $m++) {
|
||||
$projects = [];
|
||||
if (isset($monthData[$m])) {
|
||||
foreach ($monthData[$m] as $pid => $usersData) {
|
||||
$proj = $pid ? ($projectMap[$pid] ?? null) : null;
|
||||
$users = [];
|
||||
$projectTotal = 0;
|
||||
foreach ($usersData as $uid => $ud) {
|
||||
$hours = round($ud['minutes'] / 60, 1);
|
||||
$projectTotal += $hours;
|
||||
$users[] = [
|
||||
'user_id' => $uid,
|
||||
'user_name' => $userNames[$uid] ?? "User #$uid",
|
||||
'hours' => $hours,
|
||||
];
|
||||
}
|
||||
usort($users, fn ($a, $b) => $b['hours'] <=> $a['hours']);
|
||||
$projects[] = [
|
||||
'project_id' => $pid ?: null,
|
||||
'project_number' => $proj ? $proj['project_number'] : null,
|
||||
'project_name' => $proj ? $proj['name'] : null,
|
||||
'hours' => round($projectTotal, 1),
|
||||
'users' => $users,
|
||||
];
|
||||
}
|
||||
usort($projects, fn ($a, $b) => $b['hours'] <=> $a['hours']);
|
||||
}
|
||||
$months[$m] = [
|
||||
'month' => $m,
|
||||
'month_name' => getCzechMonthName($m),
|
||||
'projects' => $projects,
|
||||
];
|
||||
}
|
||||
|
||||
successResponse(['months' => $months, 'year' => $yearInt]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Single month mode
|
||||
$month = $monthParam ?? date('Y-m');
|
||||
$startDate = "{$month}-01";
|
||||
$endDate = date('Y-m-t', strtotime($startDate));
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT a.user_id, a.id as attendance_id, a.arrival_time, a.departure_time, a.break_start, a.break_end,
|
||||
CONCAT(u.first_name, ' ', u.last_name) as user_name
|
||||
FROM attendance a
|
||||
JOIN users u ON a.user_id = u.id
|
||||
WHERE a.shift_date BETWEEN ? AND ?
|
||||
AND a.departure_time IS NOT NULL
|
||||
AND (a.leave_type IS NULL OR a.leave_type = 'work')
|
||||
ORDER BY u.last_name ASC
|
||||
");
|
||||
$stmt->execute([$startDate, $endDate]);
|
||||
$workRecords = $stmt->fetchAll();
|
||||
|
||||
$userTotalMinutes = [];
|
||||
$userNames = [];
|
||||
$attendanceIds = [];
|
||||
foreach ($workRecords as $rec) {
|
||||
$uid = $rec['user_id'];
|
||||
$attendanceIds[] = $rec['attendance_id'];
|
||||
$userNames[$uid] = $rec['user_name'];
|
||||
if (!isset($userTotalMinutes[$uid])) {
|
||||
$userTotalMinutes[$uid] = 0;
|
||||
}
|
||||
$userTotalMinutes[$uid] += calculateWorkMinutes($rec);
|
||||
}
|
||||
|
||||
$aggregated = [];
|
||||
$projectIds = [];
|
||||
$userLoggedMinutes = [];
|
||||
|
||||
if (!empty($attendanceIds)) {
|
||||
$placeholders = implode(',', array_fill(0, count($attendanceIds), '?'));
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT pl.project_id, pl.started_at, pl.ended_at, pl.hours, pl.minutes AS mins, a.user_id
|
||||
FROM attendance_project_logs pl
|
||||
JOIN attendance a ON pl.attendance_id = a.id
|
||||
WHERE pl.attendance_id IN ($placeholders)
|
||||
AND (pl.hours IS NOT NULL OR pl.ended_at IS NOT NULL)
|
||||
");
|
||||
$stmt->execute($attendanceIds);
|
||||
$logs = $stmt->fetchAll();
|
||||
|
||||
foreach ($logs as $log) {
|
||||
$uid = $log['user_id'];
|
||||
$pid = (int)$log['project_id'];
|
||||
$key = "{$uid}_{$pid}";
|
||||
$projectIds[$pid] = true;
|
||||
if ($log['hours'] !== null) {
|
||||
$minutes = (int)$log['hours'] * 60 + (int)$log['mins'];
|
||||
} else {
|
||||
$minutes = max(0, (strtotime($log['ended_at']) - strtotime($log['started_at'])) / 60);
|
||||
}
|
||||
|
||||
if (!isset($aggregated[$key])) {
|
||||
$aggregated[$key] = ['user_id' => $uid, 'project_id' => $pid, 'minutes' => 0];
|
||||
}
|
||||
$aggregated[$key]['minutes'] += $minutes;
|
||||
|
||||
if (!isset($userLoggedMinutes[$uid])) {
|
||||
$userLoggedMinutes[$uid] = 0;
|
||||
}
|
||||
$userLoggedMinutes[$uid] += $minutes;
|
||||
}
|
||||
}
|
||||
|
||||
// "Bez projektu" per user
|
||||
foreach ($userTotalMinutes as $uid => $total) {
|
||||
$logged = $userLoggedMinutes[$uid] ?? 0;
|
||||
$unlogged = $total - $logged;
|
||||
if ($unlogged > 1) {
|
||||
$key = "{$uid}_0";
|
||||
if (!isset($aggregated[$key])) {
|
||||
$aggregated[$key] = ['user_id' => $uid, 'project_id' => 0, 'minutes' => 0];
|
||||
}
|
||||
$aggregated[$key]['minutes'] += $unlogged;
|
||||
}
|
||||
}
|
||||
|
||||
$projectMap = [];
|
||||
if (!empty($projectIds)) {
|
||||
try {
|
||||
$offersPdo = db();
|
||||
$ids = array_keys($projectIds);
|
||||
$placeholders = implode(',', array_fill(0, count($ids), '?'));
|
||||
$stmt = $offersPdo->prepare("SELECT id, project_number, name FROM projects WHERE id IN ($placeholders)");
|
||||
$stmt->execute($ids);
|
||||
foreach ($stmt->fetchAll() as $p) {
|
||||
$projectMap[$p['id']] = $p;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
error_log('Failed to fetch project names for report: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$report = [];
|
||||
foreach ($aggregated as $item) {
|
||||
$pid = $item['project_id'];
|
||||
$proj = $pid ? ($projectMap[$pid] ?? null) : null;
|
||||
$report[] = [
|
||||
'user_id' => $item['user_id'],
|
||||
'user_name' => $userNames[$item['user_id']] ?? "User #{$item['user_id']}",
|
||||
'project_id' => $pid ?: null,
|
||||
'project_number' => $proj ? $proj['project_number'] : null,
|
||||
'project_name' => $proj ? $proj['name'] : null,
|
||||
'hours' => round($item['minutes'] / 60, 2),
|
||||
];
|
||||
}
|
||||
|
||||
successResponse([
|
||||
'report' => $report,
|
||||
'month' => $month,
|
||||
]);
|
||||
}
|
||||
|
||||
function handleGetPrint(PDO $pdo): void
|
||||
{
|
||||
$month = validateMonth();
|
||||
$filterUserId = isset($_GET['user_id']) && $_GET['user_id'] !== '' ? (int)$_GET['user_id'] : null;
|
||||
|
||||
$year = (int)substr($month, 0, 4);
|
||||
$monthNum = (int)substr($month, 5, 2);
|
||||
|
||||
$startDate = "{$month}-01";
|
||||
$endDate = date('Y-m-t', strtotime($startDate));
|
||||
|
||||
$stmt = $pdo->query(
|
||||
"SELECT id, CONCAT(first_name, ' ', last_name) as name
|
||||
FROM users WHERE is_active = 1 ORDER BY last_name"
|
||||
);
|
||||
$users = $stmt->fetchAll();
|
||||
|
||||
$sql = "
|
||||
SELECT a.*, CONCAT(u.first_name, ' ', u.last_name) as user_name
|
||||
FROM attendance a
|
||||
JOIN users u ON a.user_id = u.id
|
||||
WHERE a.shift_date BETWEEN ? AND ?
|
||||
";
|
||||
$params = [$startDate, $endDate];
|
||||
|
||||
if ($filterUserId) {
|
||||
$sql .= ' AND a.user_id = ?';
|
||||
$params[] = $filterUserId;
|
||||
}
|
||||
|
||||
$sql .= ' ORDER BY u.last_name ASC, a.shift_date ASC';
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$records = $stmt->fetchAll();
|
||||
|
||||
enrichRecordsWithProjectLogs($pdo, $records);
|
||||
|
||||
$userTotals = calculateUserTotals($records, true);
|
||||
$leaveBalances = getLeaveBalancesBatch($pdo, array_keys($userTotals), $year);
|
||||
addFundDataToUserTotals($pdo, $userTotals, $year, $monthNum);
|
||||
|
||||
$selectedUserName = '';
|
||||
if ($filterUserId) {
|
||||
$stmt = $pdo->prepare("SELECT CONCAT(first_name, ' ', last_name) as name FROM users WHERE id = ?");
|
||||
$stmt->execute([$filterUserId]);
|
||||
$user = $stmt->fetch();
|
||||
$selectedUserName = $user ? $user['name'] : '';
|
||||
}
|
||||
|
||||
$fund = CzechHolidays::getMonthlyWorkFund($year, $monthNum);
|
||||
|
||||
successResponse([
|
||||
'user_totals' => $userTotals,
|
||||
'leave_balances' => $leaveBalances,
|
||||
'users' => $users,
|
||||
'month' => $month,
|
||||
'month_name' => getCzechMonthName($monthNum) . ' ' . $year,
|
||||
'selected_user' => $filterUserId,
|
||||
'selected_user_name' => $selectedUserName,
|
||||
'year' => $year,
|
||||
'fund' => $fund,
|
||||
]);
|
||||
}
|
||||
Reference in New Issue
Block a user