fix: oprava kritických bezpečnostních chyb a bugů z code review

- SEC-1: nahrazen exec('fsutil') za PHP-native is_link()+realpath() v NasFileManager - eliminace command injection
- SEC-2: přidáno ověření aktuálního hesla při změně hesla (profile.php + DashProfile.jsx)
- BUG-1: attendance punch obalen do transakce s SELECT FOR UPDATE - prevence race condition při dvojkliku
- BUG-2: eliminován N+1 SQL dotaz pro VAT v invoice listu - výpočet přesunut do subquery
- BUG-5/6: delete a update attendance záznamů obaleny do transakcí - prevence nekonzistentního stavu
- BUG-7: opravena duplikace nabídky - přidáno chybějící pole unit v offer items

ESLint: 0 errors | PHPCS: 0 errors | Build: OK

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-17 13:46:20 +01:00
parent 913344b8c4
commit 5550358b15
45 changed files with 373 additions and 301 deletions

View File

@@ -261,106 +261,122 @@ function handlePunch(PDO $pdo, int $userId): void
$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;
$address = !empty($input['address']) ? mb_substr($input['address'], 0, 500) : null;
$stmt = $pdo->prepare("
SELECT id, user_id, shift_date, arrival_time, break_start, break_end,
departure_time, notes, project_id, leave_type, created_at
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();
$pdo->beginTransaction();
try {
$stmt = $pdo->prepare("
SELECT id, user_id, shift_date, arrival_time, break_start, break_end,
departure_time, notes, project_id, leave_type, created_at
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
FOR UPDATE
");
$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]);
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');
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));
$pdo->commit();
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']]);
$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']]);
}
$pdo->commit();
successResponse(null, 'Pauza zaznamenána');
} else {
$pdo->rollBack();
errorResponse('Nelze zadat pauzu');
}
break;
$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']]);
case 'departure':
if ($ongoingShift['arrival_time'] && !$ongoingShift['departure_time']) {
$now = roundDownTo15Minutes($rawNow);
// 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']]);
// 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;
AuditLog::logUpdate('attendance', $ongoingShift['id'], [], [
'departure_time' => $now,
'location' => $address,
], 'Odchod zaznamenán');
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)));
successResponse(null, 'Odchod zaznamenán');
} else {
errorResponse('Nelze zadat odchod');
}
break;
$sql = 'UPDATE attendance SET break_start = ?, break_end = ? WHERE id = ?';
$stmt = $pdo->prepare($sql);
$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)));
default:
errorResponse('Neplatná akce');
$sql = 'UPDATE attendance SET break_start = ?, break_end = ? WHERE id = ?';
$stmt = $pdo->prepare($sql);
$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');
$pdo->commit();
successResponse(null, 'Odchod zaznamenán');
} else {
$pdo->rollBack();
errorResponse('Nelze zadat odchod');
}
break;
default:
$pdo->rollBack();
errorResponse('Neplatná akce');
}
} else {
$pdo->rollBack();
errorResponse('Neplatná akce - nemáte aktivní směnu');
}
} else {
errorResponse('Neplatná akce - nemáte aktivní směnu');
} catch (\Throwable $e) {
$pdo->rollBack();
throw $e;
}
}

View File

@@ -213,6 +213,8 @@ function handleGetList(PDO $pdo): void
c.name as customer_name,
(SELECT COALESCE(SUM(ii.quantity * ii.unit_price), 0)
FROM invoice_items ii WHERE ii.invoice_id = i.id) as subtotal,
(SELECT COALESCE(SUM(ii.quantity * ii.unit_price * ii.vat_rate / 100), 0)
FROM invoice_items ii WHERE ii.invoice_id = i.id) as vat_amount,
o.order_number
{$from} {$where}
ORDER BY {$p['sort']} {$p['order']}",
@@ -222,20 +224,11 @@ function handleGetList(PDO $pdo): void
$invoices = $result['items'];
// Dopocitat celkovou castku s DPH
foreach ($invoices as &$inv) {
$subtotal = (float) $inv['subtotal'];
if ($inv['apply_vat']) {
$vatStmt = $pdo->prepare('
SELECT COALESCE(SUM(quantity * unit_price * vat_rate / 100), 0)
FROM invoice_items WHERE invoice_id = ?
');
$vatStmt->execute([$inv['id']]);
$vatAmount = (float) $vatStmt->fetchColumn();
$inv['total'] = $subtotal + $vatAmount;
} else {
$inv['total'] = $subtotal;
}
$inv['total'] = $inv['apply_vat']
? $subtotal + (float) $inv['vat_amount']
: $subtotal;
}
unset($inv);

View File

@@ -428,6 +428,7 @@ function handleDuplicate(PDO $pdo, int $sourceId): void
'description' => $item['description'],
'item_description' => $item['item_description'],
'quantity' => $item['quantity'],
'unit' => $item['unit'] ?? '',
'unit_price' => $item['unit_price'],
'is_included_in_total' => $item['is_included_in_total'],
'position' => $item['position'],

View File

@@ -34,10 +34,10 @@ try {
$pdo = db();
$userId = $authData['user_id'];
// Get existing user
// Get existing user (vcetne password_hash pro overeni aktualniho hesla)
$stmt = $pdo->prepare('
SELECT id, username, email, first_name, last_name, role_id, is_active,
last_login, created_at
last_login, created_at, password_hash
FROM users WHERE id = ?
');
$stmt->execute([$userId]);
@@ -75,6 +75,14 @@ try {
// Update user
if (!empty($input['password'])) {
// Overeni aktualniho hesla
if (empty($input['current_password'])) {
errorResponse('Pro změnu hesla je nutné zadat aktuální heslo');
}
if (!password_verify($input['current_password'], $existingUser['password_hash'])) {
errorResponse('Aktuální heslo není správné');
}
// Validate password length
if (strlen($input['password']) < 8) {
errorResponse('Heslo musí mít alespoň 8 znaků');

View File

@@ -579,27 +579,34 @@ function handleUpdateAttendance(PDO $pdo, int $recordId): void
$projectLogs = $input['project_logs'] ?? null;
if ($projectLogs !== null) {
$stmt = $pdo->prepare('DELETE FROM attendance_project_logs WHERE attendance_id = ?');
$stmt->execute([$recordId]);
$pdo->beginTransaction();
try {
$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;
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]);
}
$h = (int)($log['hours'] ?? 0);
$m = (int)($log['minutes'] ?? 0);
if ($h === 0 && $m === 0) {
continue;
}
$logStmt->execute([$recordId, $pid, $h, $m]);
}
$pdo->commit();
} catch (\Throwable $e) {
$pdo->rollBack();
throw $e;
}
}
@@ -621,18 +628,26 @@ function handleDeleteAttendance(PDO $pdo, int $recordId): void
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);
$pdo->beginTransaction();
try {
$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]);
$pdo->commit();
} catch (\Throwable $e) {
$pdo->rollBack();
throw $e;
}
$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');

View File

@@ -560,12 +560,12 @@ class NasFileManager
return false;
}
$attr = @exec('fsutil reparsepoint query "' . str_replace('/', '\\', $path) . '" 2>NUL');
if ($attr !== false && $attr !== '') {
// PHP is_link detekuje symlinky
if (is_link($path)) {
return true;
}
// Fallback - realpath se lisi od puvodniho path u junction
// Junction detekce pres porovnani realpath vs zadana cesta
$real = realpath($path);
$normalized = str_replace('\\', '/', $path);
$normalReal = str_replace('\\', '/', (string) $real);

View File

@@ -261,106 +261,121 @@ function handlePunch(PDO $pdo, int $userId): void
$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;
$address = !empty($input['address']) ? mb_substr($input['address'], 0, 500) : null;
$stmt = $pdo->prepare("
SELECT id, user_id, shift_date, arrival_time, break_start, break_end,
departure_time, notes, project_id, leave_type, created_at
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();
$pdo->beginTransaction();
try {
$stmt = $pdo->prepare("
SELECT id, user_id, shift_date, arrival_time, break_start, break_end,
departure_time, notes, project_id, leave_type, created_at
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
FOR UPDATE
");
$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]);
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');
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));
$pdo->commit();
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']]);
$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']]);
}
$pdo->commit();
successResponse(null, 'Pauza zaznamenána');
} else {
$pdo->rollBack();
errorResponse('Nelze zadat pauzu');
}
break;
$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']]);
case 'departure':
if ($ongoingShift['arrival_time'] && !$ongoingShift['departure_time']) {
$now = roundDownTo15Minutes($rawNow);
// 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']]);
// 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;
AuditLog::logUpdate('attendance', $ongoingShift['id'], [], [
'departure_time' => $now,
'location' => $address,
], 'Odchod zaznamenán');
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)));
successResponse(null, 'Odchod zaznamenán');
} else {
errorResponse('Nelze zadat odchod');
}
break;
$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)));
default:
errorResponse('Neplatná akce');
$sql = 'UPDATE attendance SET break_start = ?, break_end = ? WHERE id = ?';
$stmt = $pdo->prepare($sql);
$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');
$pdo->commit();
successResponse(null, 'Odchod zaznamenán');
} else {
$pdo->rollBack();
errorResponse('Nelze zadat odchod');
}
break;
default:
$pdo->rollBack();
errorResponse('Neplatná akce');
}
} else {
$pdo->rollBack();
errorResponse('Neplatná akce - nemáte aktivní směnu');
}
} else {
errorResponse('Neplatná akce - nemáte aktivní směnu');
} catch (\Throwable $e) {
$pdo->rollBack();
throw $e;
}
}

View File

@@ -213,6 +213,8 @@ function handleGetList(PDO $pdo): void
c.name as customer_name,
(SELECT COALESCE(SUM(ii.quantity * ii.unit_price), 0)
FROM invoice_items ii WHERE ii.invoice_id = i.id) as subtotal,
(SELECT COALESCE(SUM(ii.quantity * ii.unit_price * ii.vat_rate / 100), 0)
FROM invoice_items ii WHERE ii.invoice_id = i.id) as vat_amount,
o.order_number
{$from} {$where}
ORDER BY {$p['sort']} {$p['order']}",
@@ -222,20 +224,11 @@ function handleGetList(PDO $pdo): void
$invoices = $result['items'];
// Dopocitat celkovou castku s DPH
foreach ($invoices as &$inv) {
$subtotal = (float) $inv['subtotal'];
if ($inv['apply_vat']) {
$vatStmt = $pdo->prepare('
SELECT COALESCE(SUM(quantity * unit_price * vat_rate / 100), 0)
FROM invoice_items WHERE invoice_id = ?
');
$vatStmt->execute([$inv['id']]);
$vatAmount = (float) $vatStmt->fetchColumn();
$inv['total'] = $subtotal + $vatAmount;
} else {
$inv['total'] = $subtotal;
}
$inv['total'] = $inv['apply_vat']
? $subtotal + (float) $inv['vat_amount']
: $subtotal;
}
unset($inv);

View File

@@ -428,6 +428,7 @@ function handleDuplicate(PDO $pdo, int $sourceId): void
'description' => $item['description'],
'item_description' => $item['item_description'],
'quantity' => $item['quantity'],
'unit' => $item['unit'] ?? '',
'unit_price' => $item['unit_price'],
'is_included_in_total' => $item['is_included_in_total'],
'position' => $item['position'],

View File

@@ -34,10 +34,10 @@ try {
$pdo = db();
$userId = $authData['user_id'];
// Get existing user
// Get existing user (vcetne password_hash pro overeni aktualniho hesla)
$stmt = $pdo->prepare('
SELECT id, username, email, first_name, last_name, role_id, is_active,
last_login, created_at
last_login, created_at, password_hash
FROM users WHERE id = ?
');
$stmt->execute([$userId]);
@@ -75,6 +75,14 @@ try {
// Update user
if (!empty($input['password'])) {
// Overeni aktualniho hesla
if (empty($input['current_password'])) {
errorResponse('Pro změnu hesla je nutné zadat aktuální heslo');
}
if (!password_verify($input['current_password'], $existingUser['password_hash'])) {
errorResponse('Aktuální heslo není správné');
}
// Validate password length
if (strlen($input['password']) < 8) {
errorResponse('Heslo musí mít alespoň 8 znaků');

View File

@@ -579,27 +579,34 @@ function handleUpdateAttendance(PDO $pdo, int $recordId): void
$projectLogs = $input['project_logs'] ?? null;
if ($projectLogs !== null) {
$stmt = $pdo->prepare('DELETE FROM attendance_project_logs WHERE attendance_id = ?');
$stmt->execute([$recordId]);
$pdo->beginTransaction();
try {
$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;
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]);
}
$h = (int)($log['hours'] ?? 0);
$m = (int)($log['minutes'] ?? 0);
if ($h === 0 && $m === 0) {
continue;
}
$logStmt->execute([$recordId, $pid, $h, $m]);
}
$pdo->commit();
} catch (\Throwable $e) {
$pdo->rollBack();
throw $e;
}
}
@@ -621,18 +628,26 @@ function handleDeleteAttendance(PDO $pdo, int $recordId): void
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);
$pdo->beginTransaction();
try {
$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]);
$pdo->commit();
} catch (\Throwable $e) {
$pdo->rollBack();
throw $e;
}
$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');

View File

@@ -560,12 +560,12 @@ class NasFileManager
return false;
}
$attr = @exec('fsutil reparsepoint query "' . str_replace('/', '\\', $path) . '" 2>NUL');
if ($attr !== false && $attr !== '') {
// PHP is_link detekuje symlinky
if (is_link($path)) {
return true;
}
// Fallback - realpath se lisi od puvodniho path u junction
// Junction detekce pres porovnani realpath vs zadana cesta
$real = realpath($path);
$normalized = str_replace('\\', '/', $path);
$normalReal = str_replace('\\', '/', (string) $real);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
import{j as e,m as f}from"./vendor-animation-0s3FMHwK.js";import{r as m}from"./vendor-react-BVs3cwbi.js";import{a9 as T}from"./vendor-utils-Dyr8OjFr.js";import{a as C,u as A,c as O,F as B,A as H}from"./index-fMsy8JiX.js";import{F as I}from"./Forbidden-D25jV3Oq.js";import{c as W,b as k,g as w,d as z,e as S,a as v,h as E,i as y,f as b}from"./attendanceHelpers-D6sLEw0q.js";const L="/api/admin",R=s=>s.break_start&&s.break_end?`${b(s.break_start)} - ${b(s.break_end)}`:s.break_start?`${b(s.break_start)} - ?`:"—",Z=s=>s.project_logs&&s.project_logs.length>0?e.jsx("div",{style:{display:"flex",flexDirection:"column",gap:"0.125rem"},children:s.project_logs.map((n,g)=>{let d,c,o=!1;if(n.hours!==null&&n.hours!==void 0)d=parseInt(n.hours)||0,c=parseInt(n.minutes)||0;else{o=!n.ended_at;const x=n.ended_at?new Date(n.ended_at):new Date,p=Math.floor((x-new Date(n.started_at))/6e4);d=Math.floor(p/60),c=p%60}return e.jsxs("span",{className:"admin-badge",style:{fontSize:"0.7rem",display:"inline-block",background:o?"var(--accent-light)":void 0},children:[n.project_name||`#${n.project_id}`," (",d,":",String(c).padStart(2,"0"),"h",o?" ▸":"",")"]},n.id||g)})}):s.project_name?e.jsx("span",{className:"admin-badge admin-badge-wrap",style:{fontSize:"0.75rem"},children:s.project_name}):"—",Y=s=>s.overtime>0?e.jsxs("span",{className:"leave-badge badge-overtime",children:["+",s.overtime,"h přesčas"]}):s.remaining>0?e.jsxs("span",{style:{color:"#dc2626"},children:["",s.remaining,"h"]}):e.jsx("span",{style:{color:"#16a34a"},children:"splněno"});function Q(){const s=C(),{user:n,hasPermission:g}=A(),[d,c]=m.useState(!0),o=m.useRef(null),[x,p]=m.useState(()=>{const a=new Date;return`${a.getFullYear()}-${String(a.getMonth()+1).padStart(2,"0")}`}),[t,D]=m.useState({records:[],month_name:"",year:new Date().getFullYear(),total_minutes:0,vacation_hours:0,sick_hours:0,holiday_hours:0,unpaid_hours:0,leave_balance:null,monthly_fund:null}),_=m.useCallback(async()=>{c(!0);try{const a=await O(`${L}/attendance.php?action=history&month=${x}`);if(a.status===401)return;const i=await a.json();i.success&&D(i.data)}catch{s.error("Nepodařilo se načíst data")}finally{c(!1)}},[x,s]);if(m.useEffect(()=>{_()},[_]),!g("attendance.history"))return e.jsx(I,{});const $=()=>{if(!o.current)return;const a=window.open("","_blank");a.document.write(`
import{j as e,m as f}from"./vendor-animation-0s3FMHwK.js";import{r as m}from"./vendor-react-BVs3cwbi.js";import{a9 as T}from"./vendor-utils-Dyr8OjFr.js";import{a as C,u as A,c as O,F as B,A as H}from"./index-CCZhiEoc.js";import{F as I}from"./Forbidden-D25jV3Oq.js";import{c as W,b as k,g as w,d as z,e as S,a as v,h as E,i as y,f as b}from"./attendanceHelpers-D6sLEw0q.js";const L="/api/admin",R=s=>s.break_start&&s.break_end?`${b(s.break_start)} - ${b(s.break_end)}`:s.break_start?`${b(s.break_start)} - ?`:"—",Z=s=>s.project_logs&&s.project_logs.length>0?e.jsx("div",{style:{display:"flex",flexDirection:"column",gap:"0.125rem"},children:s.project_logs.map((n,g)=>{let d,c,o=!1;if(n.hours!==null&&n.hours!==void 0)d=parseInt(n.hours)||0,c=parseInt(n.minutes)||0;else{o=!n.ended_at;const x=n.ended_at?new Date(n.ended_at):new Date,p=Math.floor((x-new Date(n.started_at))/6e4);d=Math.floor(p/60),c=p%60}return e.jsxs("span",{className:"admin-badge",style:{fontSize:"0.7rem",display:"inline-block",background:o?"var(--accent-light)":void 0},children:[n.project_name||`#${n.project_id}`," (",d,":",String(c).padStart(2,"0"),"h",o?" ▸":"",")"]},n.id||g)})}):s.project_name?e.jsx("span",{className:"admin-badge admin-badge-wrap",style:{fontSize:"0.75rem"},children:s.project_name}):"—",Y=s=>s.overtime>0?e.jsxs("span",{className:"leave-badge badge-overtime",children:["+",s.overtime,"h přesčas"]}):s.remaining>0?e.jsxs("span",{style:{color:"#dc2626"},children:["",s.remaining,"h"]}):e.jsx("span",{style:{color:"#16a34a"},children:"splněno"});function Q(){const s=C(),{user:n,hasPermission:g}=A(),[d,c]=m.useState(!0),o=m.useRef(null),[x,p]=m.useState(()=>{const a=new Date;return`${a.getFullYear()}-${String(a.getMonth()+1).padStart(2,"0")}`}),[t,D]=m.useState({records:[],month_name:"",year:new Date().getFullYear(),total_minutes:0,vacation_hours:0,sick_hours:0,holiday_hours:0,unpaid_hours:0,leave_balance:null,monthly_fund:null}),_=m.useCallback(async()=>{c(!0);try{const a=await O(`${L}/attendance.php?action=history&month=${x}`);if(a.status===401)return;const i=await a.json();i.success&&D(i.data)}catch{s.error("Nepodařilo se načíst data")}finally{c(!1)}},[x,s]);if(m.useEffect(()=>{_()},[_]),!g("attendance.history"))return e.jsx(I,{});const $=()=>{if(!o.current)return;const a=window.open("","_blank");a.document.write(`
<!DOCTYPE html>
<html lang="cs">
<head>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
import{j as e,m as p,A as Z}from"./vendor-animation-0s3FMHwK.js";import{r as i,L as J}from"./vendor-react-BVs3cwbi.js";import{a9 as G}from"./vendor-utils-Dyr8OjFr.js";import{a as q,u as Q,c as b,b as X,F as r,A as C,f as l,C as ee}from"./index-fMsy8JiX.js";import{F as se}from"./Forbidden-D25jV3Oq.js";import{b as $}from"./attendanceHelpers-D6sLEw0q.js";const N="/api/admin";function de(){const d=q(),{hasPermission:L}=Q(),[k,D]=i.useState(!0),[j,V]=i.useState(()=>{const s=new Date;return`${s.getFullYear()}-${String(s.getMonth()+1).padStart(2,"0")}-01`}),[g,A]=i.useState(()=>{const s=new Date,t=new Date(s.getFullYear(),s.getMonth()+1,0).getDate();return`${s.getFullYear()}-${String(s.getMonth()+1).padStart(2,"0")}-${String(t).padStart(2,"0")}`}),[m,F]=i.useState(""),[h,E]=i.useState(""),[P,B]=i.useState({trips:[],vehicles:[],users:[],totals:{total:0,business:0,count:0}}),[n,I]=i.useState(null),w=i.useRef(null),[T,v]=i.useState(!1),[_,U]=i.useState(null),[a,o]=i.useState({vehicle_id:"",trip_date:"",start_km:"",end_km:"",route_from:"",route_to:"",is_business:1,notes:""}),[u,z]=i.useState({show:!1,trip:null}),y=i.useCallback(async(s=!0)=>{s&&D(!0);try{let t=`${N}/trips.php?action=admin&date_from=${j}&date_to=${g}`;m&&(t+=`&vehicle_id=${m}`),h&&(t+=`&user_id=${h}`);const c=await(await b(t)).json();c.success&&B(c.data)}catch{d.error("Nepodařilo se načíst data")}finally{s&&D(!1)}},[j,g,m,h,d]);if(i.useEffect(()=>{y()},[y]),X(T),!L("trips.admin"))return e.jsx(se,{});const H=s=>{U(s),o({vehicle_id:s.vehicle_id,trip_date:s.trip_date,start_km:s.start_km,end_km:s.end_km,route_from:s.route_from,route_to:s.route_to,is_business:s.is_business,notes:s.notes||""}),v(!0)},O=async()=>{if(parseInt(a.end_km)<=parseInt(a.start_km)){d.error("Konečný stav km musí být větší než počáteční");return}try{const t=await(await b(`${N}/trips.php?id=${_.id}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)})).json();t.success?(v(!1),await y(!1),await new Promise(x=>setTimeout(x,300)),d.success(t.message)):d.error(t.error)}catch{d.error("Chyba připojení")}},W=async()=>{if(u.trip)try{const t=await(await b(`${N}/trips.php?id=${u.trip.id}`,{method:"DELETE"})).json();t.success?(z({show:!1,trip:null}),await y(!1),d.success(t.message)):d.error(t.error)}catch{d.error("Chyba připojení")}},K=async()=>{try{let s=`${N}/trips.php?action=print&date_from=${j}&date_to=${g}`;m&&(s+=`&vehicle_id=${m}`),h&&(s+=`&user_id=${h}`);const x=await(await b(s)).json();x.success&&(I(x.data),setTimeout(()=>{if(w.current){const c=window.open("","_blank");c.document.write(`
import{j as e,m as p,A as Z}from"./vendor-animation-0s3FMHwK.js";import{r as i,L as J}from"./vendor-react-BVs3cwbi.js";import{a9 as G}from"./vendor-utils-Dyr8OjFr.js";import{a as q,u as Q,c as b,b as X,F as r,A as C,f as l,C as ee}from"./index-CCZhiEoc.js";import{F as se}from"./Forbidden-D25jV3Oq.js";import{b as $}from"./attendanceHelpers-D6sLEw0q.js";const N="/api/admin";function de(){const d=q(),{hasPermission:L}=Q(),[k,D]=i.useState(!0),[j,V]=i.useState(()=>{const s=new Date;return`${s.getFullYear()}-${String(s.getMonth()+1).padStart(2,"0")}-01`}),[g,A]=i.useState(()=>{const s=new Date,t=new Date(s.getFullYear(),s.getMonth()+1,0).getDate();return`${s.getFullYear()}-${String(s.getMonth()+1).padStart(2,"0")}-${String(t).padStart(2,"0")}`}),[m,F]=i.useState(""),[h,E]=i.useState(""),[P,B]=i.useState({trips:[],vehicles:[],users:[],totals:{total:0,business:0,count:0}}),[n,I]=i.useState(null),w=i.useRef(null),[T,v]=i.useState(!1),[_,U]=i.useState(null),[a,o]=i.useState({vehicle_id:"",trip_date:"",start_km:"",end_km:"",route_from:"",route_to:"",is_business:1,notes:""}),[u,z]=i.useState({show:!1,trip:null}),y=i.useCallback(async(s=!0)=>{s&&D(!0);try{let t=`${N}/trips.php?action=admin&date_from=${j}&date_to=${g}`;m&&(t+=`&vehicle_id=${m}`),h&&(t+=`&user_id=${h}`);const c=await(await b(t)).json();c.success&&B(c.data)}catch{d.error("Nepodařilo se načíst data")}finally{s&&D(!1)}},[j,g,m,h,d]);if(i.useEffect(()=>{y()},[y]),X(T),!L("trips.admin"))return e.jsx(se,{});const H=s=>{U(s),o({vehicle_id:s.vehicle_id,trip_date:s.trip_date,start_km:s.start_km,end_km:s.end_km,route_from:s.route_from,route_to:s.route_to,is_business:s.is_business,notes:s.notes||""}),v(!0)},O=async()=>{if(parseInt(a.end_km)<=parseInt(a.start_km)){d.error("Konečný stav km musí být větší než počáteční");return}try{const t=await(await b(`${N}/trips.php?id=${_.id}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)})).json();t.success?(v(!1),await y(!1),await new Promise(x=>setTimeout(x,300)),d.success(t.message)):d.error(t.error)}catch{d.error("Chyba připojení")}},W=async()=>{if(u.trip)try{const t=await(await b(`${N}/trips.php?id=${u.trip.id}`,{method:"DELETE"})).json();t.success?(z({show:!1,trip:null}),await y(!1),d.success(t.message)):d.error(t.error)}catch{d.error("Chyba připojení")}},K=async()=>{try{let s=`${N}/trips.php?action=print&date_from=${j}&date_to=${g}`;m&&(s+=`&vehicle_id=${m}`),h&&(s+=`&user_id=${h}`);const x=await(await b(s)).json();x.success&&(I(x.data),setTimeout(()=>{if(w.current){const c=window.open("","_blank");c.document.write(`
<!DOCTYPE html>
<html lang="cs">
<head>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
import{j as x}from"./vendor-animation-0s3FMHwK.js";import{r as t}from"./vendor-react-BVs3cwbi.js";import{a as L,c as O}from"./index-fMsy8JiX.js";function J({column:e,sort:r,order:n}){return r!==e?null:x.jsx("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",style:{marginLeft:4,verticalAlign:"middle"},children:x.jsx("path",{d:n==="ASC"?"M18 15l-6-6-6 6":"M6 9l6 6 6-6"})})}function V(e,r="DESC"){const[n,a]=t.useState(e),[o,c]=t.useState(r),i=t.useRef(!1),S=t.useCallback(u=>{i.current=!0,a(m=>m===u?(c(h=>h==="ASC"?"DESC":"ASC"),m):(c("DESC"),u))},[]),d=i.current?n:null;return{sort:n,order:o,handleSort:S,activeSort:d}}function I(e,r=300){const[n,a]=t.useState(e);return t.useEffect(()=>{const o=setTimeout(()=>a(e),r);return()=>clearTimeout(o)},[e,r]),n}const N="/api/admin";function _(e,{dataKey:r,search:n,sort:a,order:o,page:c,perPage:i,extraParams:S,errorMsg:d="Nepodařilo se načíst data"}={}){const u=L(),[m,h]=t.useState([]),[j,D]=t.useState(!0),[w,k]=t.useState(null),l=t.useRef(null),p=S?JSON.stringify(S):"",b=I(n,300),C=t.useCallback(async()=>{l.current&&l.current.abort();const g=new AbortController;l.current=g;try{const s=new URLSearchParams;if(b&&s.set("search",b),a&&s.set("sort",a),o&&s.set("order",o),c&&s.set("page",c),i&&s.set("per_page",i),p){const R=JSON.parse(p);Object.entries(R).forEach(([y,A])=>{A&&s.set(y,A)})}const E=await O(`${N}/${e}?${s}`,{signal:g.signal});if(E.status===401)return;const f=await E.json();f.success?(h(f.data[r]||[]),f.data.pagination&&k(f.data.pagination)):u.error(f.error||d)}catch(s){if(s.name==="AbortError")return;u.error("Chyba připojení")}finally{D(!1)}},[u,e,r,b,a,o,c,i,p,d]);return t.useEffect(()=>(C(),()=>{l.current&&l.current.abort()}),[C]),{items:m,setItems:h,loading:j,pagination:w,refetch:C}}export{J as S,_ as a,V as u};
import{j as x}from"./vendor-animation-0s3FMHwK.js";import{r as t}from"./vendor-react-BVs3cwbi.js";import{a as L,c as O}from"./index-CCZhiEoc.js";function J({column:e,sort:r,order:n}){return r!==e?null:x.jsx("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",style:{marginLeft:4,verticalAlign:"middle"},children:x.jsx("path",{d:n==="ASC"?"M18 15l-6-6-6 6":"M6 9l6 6 6-6"})})}function V(e,r="DESC"){const[n,a]=t.useState(e),[o,c]=t.useState(r),i=t.useRef(!1),S=t.useCallback(u=>{i.current=!0,a(m=>m===u?(c(h=>h==="ASC"?"DESC":"ASC"),m):(c("DESC"),u))},[]),d=i.current?n:null;return{sort:n,order:o,handleSort:S,activeSort:d}}function I(e,r=300){const[n,a]=t.useState(e);return t.useEffect(()=>{const o=setTimeout(()=>a(e),r);return()=>clearTimeout(o)},[e,r]),n}const N="/api/admin";function _(e,{dataKey:r,search:n,sort:a,order:o,page:c,perPage:i,extraParams:S,errorMsg:d="Nepodařilo se načíst data"}={}){const u=L(),[m,h]=t.useState([]),[j,D]=t.useState(!0),[w,k]=t.useState(null),l=t.useRef(null),p=S?JSON.stringify(S):"",b=I(n,300),C=t.useCallback(async()=>{l.current&&l.current.abort();const g=new AbortController;l.current=g;try{const s=new URLSearchParams;if(b&&s.set("search",b),a&&s.set("sort",a),o&&s.set("order",o),c&&s.set("page",c),i&&s.set("per_page",i),p){const R=JSON.parse(p);Object.entries(R).forEach(([y,A])=>{A&&s.set(y,A)})}const E=await O(`${N}/${e}?${s}`,{signal:g.signal});if(E.status===401)return;const f=await E.json();f.success?(h(f.data[r]||[]),f.data.pagination&&k(f.data.pagination)):u.error(f.error||d)}catch(s){if(s.name==="AbortError")return;u.error("Chyba připojení")}finally{D(!1)}},[u,e,r,b,a,o,c,i,p,d]);return t.useEffect(()=>(C(),()=>{l.current&&l.current.abort()}),[C]),{items:m,setItems:h,loading:j,pagination:w,refetch:C}}export{J as S,_ as a,V as u};

2
dist/index.html vendored
View File

@@ -29,7 +29,7 @@
<link
href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Plus+Jakarta+Sans:wght@400;500;600;700&family=Urbanist:wght@400;500;600;700;800&display=swap"
rel="stylesheet" />
<script type="module" crossorigin src="/assets/index-fMsy8JiX.js"></script>
<script type="module" crossorigin src="/assets/index-CCZhiEoc.js"></script>
<link rel="modulepreload" crossorigin href="/assets/vendor-react-BVs3cwbi.js">
<link rel="modulepreload" crossorigin href="/assets/vendor-animation-0s3FMHwK.js">
<link rel="modulepreload" crossorigin href="/assets/vendor-utils-Dyr8OjFr.js">

View File

@@ -22,7 +22,7 @@ export default function DashProfile({
const [showModal, setShowModal] = useState(false)
const [formData, setFormData] = useState({
username: '', email: '', password: '', first_name: '', last_name: ''
username: '', email: '', password: '', current_password: '', first_name: '', last_name: ''
})
useModalLock(showModal)
@@ -33,6 +33,7 @@ export default function DashProfile({
username: user?.username || '',
email: user?.email || '',
password: '',
current_password: '',
first_name: nameParts[0] || '',
last_name: nameParts.slice(1).join(' ') || ''
})
@@ -179,6 +180,12 @@ export default function DashProfile({
<label className="admin-form-label">Nové heslo (ponechte prázdné pro zachování stávajícího)</label>
<input type="password" value={formData.password} onChange={(e) => setFormData({ ...formData, password: e.target.value })} className="admin-form-input" />
</div>
{formData.password && (
<div className="admin-form-group">
<label className="admin-form-label required">Aktuální heslo</label>
<input type="password" value={formData.current_password} onChange={(e) => setFormData({ ...formData, current_password: e.target.value })} className="admin-form-input" placeholder="Zadejte aktuální heslo pro potvrzení" />
</div>
)}
</div>
</div>
<div className="admin-modal-footer">