From 5529219234ddbc638bec800c7824f0590857b1a7 Mon Sep 17 00:00:00 2001 From: Simon Date: Thu, 12 Mar 2026 20:48:05 +0100 Subject: [PATCH] refactor: sjednoceni zdroje casu na MySQL NOW() + audit log cleanup a UI - attendance handlery pouzivaji getDbNow() misto PHP date() - nova helper funkce getDbNow() v AttendanceHelpers.php - audit log: cleanup endpoint (POST) s volbou stari zaznamu - audit log: filtry na jednom radku - dashboard: aktivita prejmenovana na Audit log s odkazem Co-Authored-By: Claude Opus 4.6 --- api/admin/audit-log.php | 34 ++++++- api/admin/handlers/attendance-handlers.php | 18 ++-- api/includes/AttendanceHelpers.php | 20 +++- .../components/dashboard/DashActivityFeed.jsx | 6 +- src/admin/pages/AuditLog.jsx | 96 ++++++++++++++++++- 5 files changed, 157 insertions(+), 17 deletions(-) diff --git a/api/admin/audit-log.php b/api/admin/audit-log.php index e55de0a..aabca73 100644 --- a/api/admin/audit-log.php +++ b/api/admin/audit-log.php @@ -26,12 +26,44 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { exit; } -if ($_SERVER['REQUEST_METHOD'] !== 'GET') { +if (!in_array($_SERVER['REQUEST_METHOD'], ['GET', 'POST'], true)) { errorResponse('Method not allowed', 405); } requirePermission($authData, 'settings.audit'); +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $input = getJsonInput(); + $action = $input['action'] ?? ''; + + if ($action !== 'cleanup') { + errorResponse('Neplatná akce'); + } + + $days = (int) ($input['days'] ?? 90); + $pdo = db(); + + if ($days === 0) { + $stmt = $pdo->query('DELETE FROM audit_logs'); + $deleted = $stmt->rowCount(); + $msg = $deleted > 0 + ? "Smazáno všech $deleted záznamů" + : 'Audit log je prázdný'; + } else { + $days = max(1, $days); + $stmt = $pdo->prepare( + 'DELETE FROM audit_logs WHERE created_at < DATE_SUB(NOW(), INTERVAL ? DAY)' + ); + $stmt->execute([$days]); + $deleted = $stmt->rowCount(); + $msg = $deleted > 0 + ? "Smazáno $deleted záznamů starších $days dní" + : "Žádné záznamy starší než $days dní nebyly nalezeny"; + } + + successResponse(['deleted' => $deleted], $msg); +} + $page = max(1, (int) ($_GET['page'] ?? 1)); $perPage = max(1, min(100, (int) ($_GET['per_page'] ?? 50))); diff --git a/api/admin/handlers/attendance-handlers.php b/api/admin/handlers/attendance-handlers.php index d33e826..bd49619 100644 --- a/api/admin/handlers/attendance-handlers.php +++ b/api/admin/handlers/attendance-handlers.php @@ -4,7 +4,8 @@ declare(strict_types=1); function handleGetCurrent(PDO $pdo, int $userId): void { - $today = date('Y-m-d'); + $dbTime = getDbNow($pdo); + $today = $dbTime['today']; $stmt = $pdo->prepare(" SELECT id, user_id, shift_date, arrival_time, arrival_lat, arrival_lng, @@ -68,13 +69,13 @@ function handleGetCurrent(PDO $pdo, int $userId): void $leaveBalance = getLeaveBalance($pdo, $userId); - $currentYear = (int)date('Y'); - $currentMonth = (int)date('m'); + $currentYear = $dbTime['year']; + $currentMonth = $dbTime['month']; $fund = CzechHolidays::getMonthlyWorkFund($currentYear, $currentMonth); $businessDays = CzechHolidays::getBusinessDaysInMonth($currentYear, $currentMonth); - $startDate = date('Y-m-01'); - $endDate = date('Y-m-t'); + $startDate = substr($dbTime['today'], 0, 7) . '-01'; + $endDate = date('Y-m-t', strtotime($startDate)); $stmt = $pdo->prepare(' SELECT id, user_id, shift_date, arrival_time, break_start, break_end, @@ -253,8 +254,9 @@ 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'); + $dbTime = getDbNow($pdo); + $today = $dbTime['today']; + $rawNow = $dbTime['now']; $lat = isset($input['latitude']) && $input['latitude'] !== '' ? (float)$input['latitude'] : null; $lng = isset($input['longitude']) && $input['longitude'] !== '' ? (float)$input['longitude'] : null; @@ -508,7 +510,7 @@ function handleSwitchProject(PDO $pdo, int $userId): void } $attendanceId = $currentShift['id']; - $now = date('Y-m-d H:i:s'); + $now = getDbNow($pdo)['now']; $stmt = $pdo->prepare( 'UPDATE attendance_project_logs SET ended_at = ? diff --git a/api/includes/AttendanceHelpers.php b/api/includes/AttendanceHelpers.php index 524b98f..1a4f770 100644 --- a/api/includes/AttendanceHelpers.php +++ b/api/includes/AttendanceHelpers.php @@ -6,6 +6,21 @@ declare(strict_types=1); +/** + * Vraci aktualni cas a datum z MySQL (jednotny zdroj casu) + * @return array{now: string, today: string, year: int, month: int} + */ +function getDbNow(PDO $pdo): array +{ + $row = $pdo->query("SELECT NOW() AS now, CURDATE() AS today, YEAR(NOW()) AS y, MONTH(NOW()) AS m")->fetch(); + return [ + 'now' => $row['now'], + 'today' => $row['today'], + 'year' => (int)$row['y'], + 'month' => (int)$row['m'], + ]; +} + function roundUpTo15Minutes(string $datetime): string { $timestamp = strtotime($datetime); @@ -327,15 +342,14 @@ function addFundDataToUserTotals(PDO $pdo, array &$userTotals, int $year, int $m } unset($ut); - $today = date('Y-m-d'); $stmt = $pdo->prepare(" SELECT DISTINCT user_id FROM attendance - WHERE shift_date = ? + WHERE shift_date = CURDATE() AND arrival_time IS NOT NULL AND departure_time IS NULL AND (leave_type IS NULL OR leave_type = 'work') "); - $stmt->execute([$today]); + $stmt->execute(); $workingNow = $stmt->fetchAll(PDO::FETCH_COLUMN); foreach ($workingNow as $uid) { if (isset($userTotals[$uid])) { diff --git a/src/admin/components/dashboard/DashActivityFeed.jsx b/src/admin/components/dashboard/DashActivityFeed.jsx index 714c3fa..919c1af 100644 --- a/src/admin/components/dashboard/DashActivityFeed.jsx +++ b/src/admin/components/dashboard/DashActivityFeed.jsx @@ -1,3 +1,4 @@ +import { Link } from 'react-router-dom' import { ENTITY_TYPE_LABELS, getActivityIconClass, formatActivityTime } from '../../utils/dashboardHelpers' function getActivityIcon(action) { @@ -43,8 +44,9 @@ export default function DashActivityFeed({ activities }) { return (
-
-

Poslední aktivita

+
+

Audit log

+ Detail →
{activities.map((act) => ( diff --git a/src/admin/pages/AuditLog.jsx b/src/admin/pages/AuditLog.jsx index 8d88567..3e7da5a 100644 --- a/src/admin/pages/AuditLog.jsx +++ b/src/admin/pages/AuditLog.jsx @@ -76,6 +76,9 @@ export default function AuditLog() { date_from: '', date_to: '', }) + const [showCleanup, setShowCleanup] = useState(false) + const [cleanupDays, setCleanupDays] = useState(90) + const [cleaning, setCleaning] = useState(false) const fetchLogs = useCallback(async (page = 1, perPage = 50) => { setLoading(true) @@ -139,6 +142,29 @@ export default function AuditLog() { fetchLogs(1, newPerPage) } + const handleCleanup = async () => { + setCleaning(true) + try { + const response = await apiFetch(`${API_BASE}/audit-log.php`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'cleanup', days: cleanupDays }), + }) + const data = await response.json() + if (data.success) { + alert.success(data.message) + setShowCleanup(false) + fetchLogs() + } else { + alert.error(data.error) + } + } catch { + alert.error('Chyba připojení') + } finally { + setCleaning(false) + } + } + const formatDatetime = (dateString) => { if (!dateString) { return '-' @@ -195,8 +221,74 @@ export default function AuditLog() {

)}
+ + {showCleanup && ( +
+
!cleaning && setShowCleanup(false)} /> + +
+
+ + + + +
+

Vyčistit audit log

+

Smazat záznamy starší než:

+
+ +
+

Tato akce je nevratná.

+
+
+ + +
+
+
+ )} +
-
+
-
-