refactor: odstraneni PSR-1 SideEffects warningu
- Handler funkce extrahovany z API souboru do api/admin/handlers/ - config.php rozdeleny na helpers.php (funkce) a constants.php (konstanty) - require_once odstranen z class souboru (AuditLog, JWTAuth, LeaveNotification) - vendor/autoload.php presunuto do config.php bootstrap - totp-handlers.php: pridany use deklarace pro TwoFactorAuth - phpstan.neon: bootstrapFiles, scanDirectories, dynamicConstantNames - Opraveny chybejici routing bloky v totp.php a session.php Vysledek: phpcs 0 errors 0 warnings, PHPStan 0 errors, ESLint 0 errors Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,9 @@ 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/AttendanceHelpers.php';
|
||||
require_once dirname(__DIR__) . '/includes/Mailer.php';
|
||||
require_once dirname(__DIR__) . '/includes/LeaveNotification.php';
|
||||
require_once __DIR__ . '/handlers/leave-requests-handlers.php';
|
||||
|
||||
// Set headers
|
||||
setCorsHeaders();
|
||||
@@ -76,462 +78,3 @@ try {
|
||||
// ============================================================================
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Calculate number of business days between two dates (skip Sat/Sun)
|
||||
*/
|
||||
function calculateBusinessDays(string $dateFrom, string $dateTo): int
|
||||
{
|
||||
$start = new DateTime($dateFrom);
|
||||
$end = new DateTime($dateTo);
|
||||
$end->modify('+1 day'); // include the end date
|
||||
|
||||
$days = 0;
|
||||
$period = new DatePeriod($start, new DateInterval('P1D'), $end);
|
||||
foreach ($period as $date) {
|
||||
$dayOfWeek = (int)$date->format('N'); // 1=Mon, 7=Sun
|
||||
if ($dayOfWeek <= 5) {
|
||||
$days++;
|
||||
}
|
||||
}
|
||||
return $days;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get leave balance for user (reuse logic from attendance.php)
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
function getLeaveBalanceForRequest(PDO $pdo, int $userId, ?int $year = null): array
|
||||
{
|
||||
$year = $year ?: (int)date('Y');
|
||||
|
||||
$stmt = $pdo->prepare('SELECT * 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'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hours already locked in pending requests for vacation
|
||||
*/
|
||||
function getPendingVacationHours(PDO $pdo, int $userId, int $year): float
|
||||
{
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT COALESCE(SUM(total_hours), 0) as pending_hours
|
||||
FROM leave_requests
|
||||
WHERE user_id = ? AND leave_type = 'vacation' AND status = 'pending'
|
||||
AND YEAR(date_from) = ?
|
||||
");
|
||||
$stmt->execute([$userId, $year]);
|
||||
return (float)$stmt->fetchColumn();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// GET Handlers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* GET - Own leave requests
|
||||
*/
|
||||
function handleGetMyRequests(PDO $pdo, int $userId): void
|
||||
{
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT lr.*,
|
||||
CONCAT(u.first_name, ' ', u.last_name) as reviewer_name
|
||||
FROM leave_requests lr
|
||||
LEFT JOIN users u ON lr.reviewer_id = u.id
|
||||
WHERE lr.user_id = ?
|
||||
ORDER BY lr.created_at DESC
|
||||
");
|
||||
$stmt->execute([$userId]);
|
||||
$requests = $stmt->fetchAll();
|
||||
|
||||
successResponse($requests);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET - All pending requests (for approver)
|
||||
*/
|
||||
function handleGetPending(PDO $pdo): void
|
||||
{
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT lr.*,
|
||||
CONCAT(u.first_name, ' ', u.last_name) as employee_name,
|
||||
CONCAT(rv.first_name, ' ', rv.last_name) as reviewer_name
|
||||
FROM leave_requests lr
|
||||
JOIN users u ON lr.user_id = u.id
|
||||
LEFT JOIN users rv ON lr.reviewer_id = rv.id
|
||||
WHERE lr.status = 'pending'
|
||||
ORDER BY lr.created_at ASC
|
||||
");
|
||||
$stmt->execute();
|
||||
$requests = $stmt->fetchAll();
|
||||
|
||||
successResponse([
|
||||
'requests' => $requests,
|
||||
'count' => count($requests),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET - All requests with filters (for approver)
|
||||
*/
|
||||
function handleGetAll(PDO $pdo): void
|
||||
{
|
||||
$status = $_GET['status'] ?? '';
|
||||
$userId = isset($_GET['user_id']) ? (int)$_GET['user_id'] : null;
|
||||
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
if ($status && in_array($status, ['pending', 'approved', 'rejected', 'cancelled'])) {
|
||||
$where[] = 'lr.status = ?';
|
||||
$params[] = $status;
|
||||
}
|
||||
|
||||
if ($userId) {
|
||||
$where[] = 'lr.user_id = ?';
|
||||
$params[] = $userId;
|
||||
}
|
||||
|
||||
$whereClause = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT lr.*,
|
||||
CONCAT(u.first_name, ' ', u.last_name) as employee_name,
|
||||
CONCAT(rv.first_name, ' ', rv.last_name) as reviewer_name
|
||||
FROM leave_requests lr
|
||||
JOIN users u ON lr.user_id = u.id
|
||||
LEFT JOIN users rv ON lr.reviewer_id = rv.id
|
||||
$whereClause
|
||||
ORDER BY lr.created_at DESC
|
||||
LIMIT 200
|
||||
");
|
||||
$stmt->execute($params);
|
||||
$requests = $stmt->fetchAll();
|
||||
|
||||
successResponse($requests);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// POST Handlers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* POST - Submit new leave request
|
||||
*/
|
||||
function handleSubmitRequest(PDO $pdo, int $userId): void
|
||||
{
|
||||
$input = getJsonInput();
|
||||
|
||||
$leaveType = $input['leave_type'] ?? '';
|
||||
$dateFrom = $input['date_from'] ?? '';
|
||||
$dateTo = $input['date_to'] ?? '';
|
||||
$notes = trim($input['notes'] ?? '');
|
||||
|
||||
if (!$leaveType || !$dateFrom || !$dateTo) {
|
||||
errorResponse('Vyplňte všechna povinná pole');
|
||||
}
|
||||
|
||||
if (!in_array($leaveType, ['vacation', 'sick', 'unpaid'])) {
|
||||
errorResponse('Neplatný typ nepřítomnosti');
|
||||
}
|
||||
|
||||
// Validate dates
|
||||
$from = new DateTime($dateFrom);
|
||||
$to = new DateTime($dateTo);
|
||||
if ($to < $from) {
|
||||
errorResponse('Datum "do" nesmí být před datem "od"');
|
||||
}
|
||||
|
||||
// Calculate business days
|
||||
$businessDays = calculateBusinessDays($dateFrom, $dateTo);
|
||||
if ($businessDays === 0) {
|
||||
errorResponse('Zvolené období neobsahuje žádné pracovní dny');
|
||||
}
|
||||
|
||||
$totalHours = $businessDays * 8;
|
||||
|
||||
// Check vacation balance
|
||||
if ($leaveType === 'vacation') {
|
||||
$year = (int)$from->format('Y');
|
||||
$balance = getLeaveBalanceForRequest($pdo, $userId, $year);
|
||||
$pendingHours = getPendingVacationHours($pdo, $userId, $year);
|
||||
$availableHours = $balance['vacation_remaining'] - $pendingHours;
|
||||
|
||||
if ($availableHours < $totalHours) {
|
||||
errorResponse(
|
||||
"Nemáte dostatek hodin dovolené. Dostupné: {$availableHours}h"
|
||||
. " (zbývá {$balance['vacation_remaining']}h, v čekajících žádostech: {$pendingHours}h),"
|
||||
. " požadujete: {$totalHours}h."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check overlapping requests
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT id FROM leave_requests
|
||||
WHERE user_id = ? AND status IN ('pending', 'approved')
|
||||
AND date_from <= ? AND date_to >= ?
|
||||
");
|
||||
$stmt->execute([$userId, $dateTo, $dateFrom]);
|
||||
if ($stmt->fetch()) {
|
||||
errorResponse('Pro toto období již existuje žádost o nepřítomnost');
|
||||
}
|
||||
|
||||
// Insert request
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO leave_requests (user_id, leave_type, date_from, date_to, total_hours, total_days, notes, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 'pending')
|
||||
");
|
||||
$stmt->execute([$userId, $leaveType, $dateFrom, $dateTo, $totalHours, $businessDays, $notes ?: null]);
|
||||
|
||||
$requestId = (int)$pdo->lastInsertId();
|
||||
|
||||
AuditLog::logCreate('leave_request', $requestId, [
|
||||
'leave_type' => $leaveType,
|
||||
'date_from' => $dateFrom,
|
||||
'date_to' => $dateTo,
|
||||
'total_days' => $businessDays,
|
||||
'total_hours' => $totalHours,
|
||||
], "Podána žádost o nepřítomnost: $leaveType ($dateFrom - $dateTo)");
|
||||
|
||||
// Send email notification
|
||||
try {
|
||||
$stmt = $pdo->prepare("SELECT CONCAT(first_name, ' ', last_name) as name FROM users WHERE id = ?");
|
||||
$stmt->execute([$userId]);
|
||||
$employeeName = $stmt->fetchColumn() ?: 'Neznámý';
|
||||
|
||||
LeaveNotification::notifyNewRequest([
|
||||
'leave_type' => $leaveType,
|
||||
'date_from' => $dateFrom,
|
||||
'date_to' => $dateTo,
|
||||
'total_days' => $businessDays,
|
||||
'total_hours' => $totalHours,
|
||||
'notes' => $notes,
|
||||
], $employeeName);
|
||||
} catch (\Exception $e) {
|
||||
error_log('Leave notification error: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
successResponse(['id' => $requestId], 'Žádost byla odeslána ke schválení');
|
||||
}
|
||||
|
||||
/**
|
||||
* POST - Cancel own pending request
|
||||
*/
|
||||
function handleCancelRequest(PDO $pdo, int $userId): void
|
||||
{
|
||||
$input = getJsonInput();
|
||||
$requestId = (int)($input['request_id'] ?? 0);
|
||||
|
||||
if (!$requestId) {
|
||||
errorResponse('ID žádosti je povinné');
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare('SELECT * FROM leave_requests WHERE id = ? AND user_id = ?');
|
||||
$stmt->execute([$requestId, $userId]);
|
||||
$request = $stmt->fetch();
|
||||
|
||||
if (!$request) {
|
||||
errorResponse('Žádost nebyla nalezena');
|
||||
}
|
||||
|
||||
if ($request['status'] !== 'pending') {
|
||||
errorResponse('Lze zrušit pouze čekající žádosti');
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("UPDATE leave_requests SET status = 'cancelled' WHERE id = ?");
|
||||
$stmt->execute([$requestId]);
|
||||
|
||||
AuditLog::logUpdate(
|
||||
'leave_request',
|
||||
$requestId,
|
||||
['status' => 'pending'],
|
||||
['status' => 'cancelled'],
|
||||
'Žádost o nepřítomnost zrušena zaměstnancem'
|
||||
);
|
||||
|
||||
successResponse(null, 'Žádost byla zrušena');
|
||||
}
|
||||
|
||||
/**
|
||||
* POST - Approve a leave request
|
||||
*
|
||||
* @param array<string, mixed> $authData
|
||||
*/
|
||||
function handleApproveRequest(PDO $pdo, int $reviewerId, array $authData): void
|
||||
{
|
||||
$input = getJsonInput();
|
||||
$requestId = (int)($input['request_id'] ?? 0);
|
||||
|
||||
if (!$requestId) {
|
||||
errorResponse('ID žádosti je povinné');
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare('SELECT * FROM leave_requests WHERE id = ?');
|
||||
$stmt->execute([$requestId]);
|
||||
$request = $stmt->fetch();
|
||||
|
||||
if (!$request) {
|
||||
errorResponse('Žádost nebyla nalezena');
|
||||
}
|
||||
|
||||
if ($request['status'] !== 'pending') {
|
||||
errorResponse('Lze schválit pouze čekající žádosti');
|
||||
}
|
||||
|
||||
if ((int)$request['user_id'] === $reviewerId && !($authData['user']['is_admin'] ?? false)) {
|
||||
errorResponse('Nemůžete schválit vlastní žádost', 403);
|
||||
}
|
||||
|
||||
// Re-check vacation balance
|
||||
if ($request['leave_type'] === 'vacation') {
|
||||
$year = (int)date('Y', strtotime($request['date_from']));
|
||||
$balance = getLeaveBalanceForRequest($pdo, (int)$request['user_id'], $year);
|
||||
|
||||
if ($balance['vacation_remaining'] < (float)$request['total_hours']) {
|
||||
errorResponse(
|
||||
"Zaměstnanec nemá dostatek hodin dovolené."
|
||||
. " Zbývá: {$balance['vacation_remaining']}h, požadováno: {$request['total_hours']}h."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Begin transaction
|
||||
$pdo->beginTransaction();
|
||||
|
||||
try {
|
||||
// Create attendance records for each business day
|
||||
$start = new DateTime($request['date_from']);
|
||||
$end = new DateTime($request['date_to']);
|
||||
$end->modify('+1 day');
|
||||
|
||||
$period = new DatePeriod($start, new DateInterval('P1D'), $end);
|
||||
$insertStmt = $pdo->prepare('
|
||||
INSERT INTO attendance (user_id, shift_date, leave_type, leave_hours, notes)
|
||||
VALUES (?, ?, ?, 8, ?)
|
||||
');
|
||||
|
||||
$leaveNote = "Schválená žádost #$requestId";
|
||||
$totalBusinessDays = 0;
|
||||
|
||||
foreach ($period as $date) {
|
||||
$dayOfWeek = (int)$date->format('N');
|
||||
if ($dayOfWeek <= 5) {
|
||||
$shiftDate = $date->format('Y-m-d');
|
||||
$insertStmt->execute([
|
||||
$request['user_id'],
|
||||
$shiftDate,
|
||||
$request['leave_type'],
|
||||
$leaveNote,
|
||||
]);
|
||||
$totalBusinessDays++;
|
||||
}
|
||||
}
|
||||
|
||||
// Update leave balance ONCE with total hours (was N queries, one per day)
|
||||
if ($totalBusinessDays > 0) {
|
||||
updateLeaveBalance(
|
||||
$pdo,
|
||||
(int)$request['user_id'],
|
||||
$request['date_from'],
|
||||
$request['leave_type'],
|
||||
(float)($totalBusinessDays * 8)
|
||||
);
|
||||
}
|
||||
|
||||
// Update request status
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE leave_requests
|
||||
SET status = 'approved', reviewer_id = ?, reviewed_at = NOW()
|
||||
WHERE id = ?
|
||||
");
|
||||
$stmt->execute([$reviewerId, $requestId]);
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
AuditLog::logUpdate(
|
||||
'leave_request',
|
||||
$requestId,
|
||||
['status' => 'pending'],
|
||||
['status' => 'approved', 'reviewer_id' => $reviewerId],
|
||||
'Žádost o nepřítomnost schválena'
|
||||
);
|
||||
|
||||
successResponse(null, 'Žádost byla schválena');
|
||||
} catch (\Exception $e) {
|
||||
$pdo->rollBack();
|
||||
error_log('Approve request error: ' . $e->getMessage());
|
||||
errorResponse('Chyba při schvalování žádosti', 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST - Reject a leave request
|
||||
*
|
||||
* @param array<string, mixed> $authData
|
||||
*/
|
||||
function handleRejectRequest(PDO $pdo, int $reviewerId, array $authData): void
|
||||
{
|
||||
$input = getJsonInput();
|
||||
$requestId = (int)($input['request_id'] ?? 0);
|
||||
$note = trim($input['note'] ?? '');
|
||||
|
||||
if (!$requestId) {
|
||||
errorResponse('ID žádosti je povinné');
|
||||
}
|
||||
|
||||
if (!$note) {
|
||||
errorResponse('Důvod zamítnutí je povinný');
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare('SELECT * FROM leave_requests WHERE id = ?');
|
||||
$stmt->execute([$requestId]);
|
||||
$request = $stmt->fetch();
|
||||
|
||||
if (!$request) {
|
||||
errorResponse('Žádost nebyla nalezena');
|
||||
}
|
||||
|
||||
if ($request['status'] !== 'pending') {
|
||||
errorResponse('Lze zamítnout pouze čekající žádosti');
|
||||
}
|
||||
|
||||
if ((int)$request['user_id'] === $reviewerId && !($authData['user']['is_admin'] ?? false)) {
|
||||
errorResponse('Nemůžete zamítnout vlastní žádost', 403);
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE leave_requests
|
||||
SET status = 'rejected', reviewer_id = ?, reviewer_note = ?, reviewed_at = NOW()
|
||||
WHERE id = ?
|
||||
");
|
||||
$stmt->execute([$reviewerId, $note, $requestId]);
|
||||
|
||||
AuditLog::logUpdate(
|
||||
'leave_request',
|
||||
$requestId,
|
||||
['status' => 'pending'],
|
||||
['status' => 'rejected', 'reviewer_id' => $reviewerId, 'reviewer_note' => $note],
|
||||
"Žádost o nepřítomnost zamítnuta: $note"
|
||||
);
|
||||
|
||||
successResponse(null, 'Žádost byla zamítnuta');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user