Initial commit
This commit is contained in:
737
api/admin/attendance.php
Normal file
737
api/admin/attendance.php
Normal file
@@ -0,0 +1,737 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* BOHA Automation - Attendance API
|
||||
*
|
||||
* Endpoints:
|
||||
* GET /api/admin/attendance.php - Get current shift status and today's shifts
|
||||
* GET /api/admin/attendance.php?action=history - Get attendance history for month
|
||||
* GET /api/admin/attendance.php?action=admin - Get all attendance records (admin)
|
||||
* GET /api/admin/attendance.php?action=balances - Get leave balances (admin)
|
||||
* GET /api/admin/attendance.php?action=workfund&year=YYYY - Get work fund overview (admin)
|
||||
* GET /api/admin/attendance.php?action=location&id=X - Get location for record (admin)
|
||||
* GET /api/admin/attendance.php?action=print - Get print data for attendance (admin)
|
||||
* GET /api/admin/attendance.php?action=projects - Get active projects list
|
||||
* GET /api/admin/attendance.php?action=project_report&month=YYYY-MM - Get project hours report (admin)
|
||||
* GET /api/admin/attendance.php?action=project_logs&attendance_id=X - Get project logs for a shift
|
||||
* POST /api/admin/attendance.php - Clock in/out/break actions
|
||||
* POST /api/admin/attendance.php?action=update_address - Doplnit adresu k poslednimu zaznamu
|
||||
* POST /api/admin/attendance.php?action=notes - Save notes for current shift
|
||||
* POST /api/admin/attendance.php?action=switch_project - Switch active project on current shift
|
||||
* POST /api/admin/attendance.php?action=leave - Add leave record
|
||||
* POST /api/admin/attendance.php?action=save_project_logs - Save project logs for record (admin)
|
||||
* POST /api/admin/attendance.php?action=create - Create attendance record (admin)
|
||||
* POST /api/admin/attendance.php?action=bulk_attendance - Bulk add attendance for month (admin)
|
||||
* POST /api/admin/attendance.php?action=balances - Update leave balance (admin)
|
||||
* PUT /api/admin/attendance.php?id=X - Update attendance record (admin)
|
||||
* DELETE /api/admin/attendance.php?id=X - Delete attendance record (admin)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once dirname(__DIR__) . '/config.php';
|
||||
require_once dirname(__DIR__) . '/includes/JWTAuth.php';
|
||||
require_once dirname(__DIR__) . '/includes/AuditLog.php';
|
||||
require_once dirname(__DIR__) . '/includes/CzechHolidays.php';
|
||||
require_once dirname(__DIR__) . '/includes/AttendanceHelpers.php';
|
||||
require_once dirname(__DIR__) . '/includes/AttendanceAdmin.php';
|
||||
|
||||
setCorsHeaders();
|
||||
setSecurityHeaders();
|
||||
setNoCacheHeaders();
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$authData = JWTAuth::requireAuth();
|
||||
AuditLog::setUser($authData['user_id'], $authData['user']['username'] ?? 'unknown');
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$action = $_GET['action'] ?? '';
|
||||
$recordId = isset($_GET['id']) ? (int) $_GET['id'] : null;
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
$userId = $authData['user_id'];
|
||||
$isAdmin = $authData['user']['is_admin'] ?? false;
|
||||
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
if ($action === 'history') {
|
||||
requirePermission($authData, 'attendance.history');
|
||||
handleGetHistory($pdo, $userId);
|
||||
} elseif ($action === 'admin') {
|
||||
requirePermission($authData, 'attendance.admin');
|
||||
handleGetAdmin($pdo);
|
||||
} elseif ($action === 'balances') {
|
||||
requirePermission($authData, 'attendance.balances');
|
||||
handleGetBalances($pdo);
|
||||
} elseif ($action === 'location' && $recordId) {
|
||||
requirePermission($authData, 'attendance.admin');
|
||||
handleGetLocation($pdo, $recordId);
|
||||
} elseif ($action === 'users') {
|
||||
requirePermission($authData, 'attendance.admin');
|
||||
handleGetUsers($pdo);
|
||||
} elseif ($action === 'print') {
|
||||
requirePermission($authData, 'attendance.admin');
|
||||
handleGetPrint($pdo);
|
||||
} elseif ($action === 'workfund') {
|
||||
requirePermission($authData, 'attendance.balances');
|
||||
handleGetWorkFund($pdo);
|
||||
} elseif ($action === 'projects') {
|
||||
handleGetProjects();
|
||||
} elseif ($action === 'project_report') {
|
||||
if (!hasPermission($authData, 'attendance.admin') && !hasPermission($authData, 'attendance.balances')) {
|
||||
requirePermission($authData, 'attendance.admin');
|
||||
}
|
||||
handleGetProjectReport($pdo);
|
||||
} elseif ($action === 'project_logs') {
|
||||
handleGetProjectLogs($pdo, $userId, $authData);
|
||||
} else {
|
||||
requirePermission($authData, 'attendance.record');
|
||||
handleGetCurrent($pdo, $userId);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
if ($action === 'leave') {
|
||||
requirePermission($authData, 'attendance.record');
|
||||
handleAddLeave($pdo, $userId);
|
||||
} elseif ($action === 'notes') {
|
||||
requirePermission($authData, 'attendance.record');
|
||||
handleSaveNotes($pdo, $userId);
|
||||
} elseif ($action === 'switch_project') {
|
||||
requirePermission($authData, 'attendance.record');
|
||||
handleSwitchProject($pdo, $userId);
|
||||
} elseif ($action === 'save_project_logs') {
|
||||
requirePermission($authData, 'attendance.admin');
|
||||
handleSaveProjectLogs($pdo);
|
||||
} elseif ($action === 'create') {
|
||||
requirePermission($authData, 'attendance.admin');
|
||||
handleCreateAttendance($pdo);
|
||||
} elseif ($action === 'bulk_attendance') {
|
||||
requirePermission($authData, 'attendance.admin');
|
||||
handleBulkAttendance($pdo);
|
||||
} elseif ($action === 'balances') {
|
||||
requirePermission($authData, 'attendance.balances');
|
||||
handleUpdateBalance($pdo);
|
||||
} elseif ($action === 'update_address') {
|
||||
requirePermission($authData, 'attendance.record');
|
||||
handleUpdateAddress($pdo, $userId);
|
||||
} else {
|
||||
requirePermission($authData, 'attendance.record');
|
||||
handlePunch($pdo, $userId);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'PUT':
|
||||
requirePermission($authData, 'attendance.admin');
|
||||
if (!$recordId) {
|
||||
errorResponse('ID záznamu je povinné');
|
||||
}
|
||||
handleUpdateAttendance($pdo, $recordId);
|
||||
break;
|
||||
|
||||
case 'DELETE':
|
||||
requirePermission($authData, 'attendance.admin');
|
||||
if (!$recordId) {
|
||||
errorResponse('ID záznamu je povinné');
|
||||
}
|
||||
handleDeleteAttendance($pdo, $recordId);
|
||||
break;
|
||||
|
||||
default:
|
||||
errorResponse('Metoda není povolena', 405);
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
error_log('Attendance API error: ' . $e->getMessage());
|
||||
errorResponse('Chyba databáze', 500);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// User-facing handlers
|
||||
// ============================================================================
|
||||
|
||||
function handleGetCurrent(PDO $pdo, int $userId): void
|
||||
{
|
||||
$today = date('Y-m-d');
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT * 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 * 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 * 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 * 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 * 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 * 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 * 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<string, mixed> $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 * 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 * 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');
|
||||
}
|
||||
Reference in New Issue
Block a user