import { useState } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { motion } from "framer-motion"; import { useAuth } from "../context/AuthContext"; import { useAlert } from "../context/AlertContext"; import Forbidden from "../components/Forbidden"; import Pagination from "../components/Pagination"; import FormField from "../components/FormField"; import AdminDatePicker from "../components/AdminDatePicker"; import { czechPlural } from "../utils/formatters"; import apiFetch from "../utils/api"; import { Skeleton } from "boneyard-js/react"; import AuditLogFixture from "../fixtures/AuditLogFixture"; const API_BASE = "/api/admin"; const ACTION_LABELS: Record = { create: "Vytvoření", update: "Úprava", delete: "Smazání", login: "Přihlášení", login_failed: "Neúspěšné přihlášení", logout: "Odhlášení", view: "Zobrazení", activate: "Aktivace", deactivate: "Deaktivace", password_change: "Změna hesla", permission_change: "Změna oprávnění", access_denied: "Přístup odepřen", }; const ACTION_BADGE_CLASS: Record = { create: "admin-badge-success", update: "admin-badge-info", delete: "admin-badge-danger", login: "admin-badge-secondary", login_failed: "admin-badge-danger", logout: "admin-badge-secondary", view: "admin-badge-info", activate: "admin-badge-success", deactivate: "admin-badge-warning", password_change: "admin-badge-info", permission_change: "admin-badge-warning", access_denied: "admin-badge-danger", }; const ENTITY_TYPE_LABELS: Record = { user: "Uživatel", attendance: "Docházka", leave_request: "Žádost o nepřítomnost", offers_quotation: "Nabídka", offers_customer: "Zákazník", offers_item_template: "Šablona položky", offers_scope_template: "Šablona rozsahu", offers_settings: "Nastavení nabídek", orders_order: "Objednávka", invoices_invoice: "Faktura", projects_project: "Projekt", role: "Role", trips: "Jízda", vehicles: "Vozidlo", bank_account: "Bankovní účet", }; const ACTION_OPTIONS = Object.entries(ACTION_LABELS).map(([value, label]) => ({ value, label, })); const ENTITY_OPTIONS = Object.entries(ENTITY_TYPE_LABELS).map( ([value, label]) => ({ value, label }), ); interface AuditLogEntry { id: number; created_at: string; username: string | null; action: string; entity_type: string | null; description: string | null; user_ip: string | null; } interface Filters { search: string; action: string; entity_type: string; date_from: string; date_to: string; } export default function AuditLog() { const { hasPermission } = useAuth(); const alert = useAlert(); const queryClient = useQueryClient(); const [filters, setFilters] = useState({ search: "", action: "", entity_type: "", date_from: "", date_to: "", }); const [page, setPage] = useState(1); const [perPage, setPerPage] = useState(50); const [showCleanup, setShowCleanup] = useState(false); const [cleanupDays, setCleanupDays] = useState(90); const [cleaning, setCleaning] = useState(false); const { data: logsData, isPending } = useQuery({ queryKey: [ "audit-log", { search: filters.search, action: filters.action, entityType: filters.entity_type, dateFrom: filters.date_from, dateTo: filters.date_to, page, perPage, }, ], queryFn: async () => { const params = new URLSearchParams({ page: String(page), per_page: String(perPage), }); if (filters.search) params.set("search", filters.search); if (filters.action) params.set("action", filters.action); if (filters.entity_type) params.set("entity_type", filters.entity_type); if (filters.date_from) params.set("date_from", filters.date_from); if (filters.date_to) params.set("date_to", filters.date_to); const response = await apiFetch( `${API_BASE}/audit-log?${params.toString()}`, ); if (response.status === 401) throw new Error("Unauthorized"); const result = await response.json(); if (!result.success) throw new Error(result.error || "Nepodařilo se načíst audit log"); return { data: Array.isArray(result.data) ? result.data : [], pagination: { total: result.pagination?.total ?? 0, page: result.pagination?.page ?? 1, per_page: result.pagination?.limit ?? perPage, total_pages: result.pagination?.total_pages ?? 1, }, }; }, }); const logs = logsData?.data ?? []; const pagination = logsData?.pagination ?? null; if (!hasPermission("settings.audit")) { return ; } const handleFilterChange = (key: keyof Filters, value: string) => { setFilters((prev) => ({ ...prev, [key]: value })); setPage(1); }; const handlePageChange = (newPage: number) => { setPage(newPage); }; const handlePerPageChange = (newPerPage: number) => { setPage(1); setPerPage(newPerPage); }; const handleCleanup = async () => { setCleaning(true); try { const response = await apiFetch(`${API_BASE}/audit-log/cleanup`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ days: cleanupDays }), }); const data = await response.json(); if (data.success) { alert.success(data.message); setShowCleanup(false); queryClient.invalidateQueries({ queryKey: ["audit-log"] }); } else { alert.error(data.error); } } catch { alert.error("Chyba připojení"); } finally { setCleaning(false); } }; const formatDatetime = (dateString: string | null): string => { if (!dateString) return "-"; return new Date(dateString).toLocaleString("cs-CZ"); }; if (isPending && logs.length === 0) { return ( } >
); } return (

Audit log

{pagination && (

{pagination.total}{" "} {czechPlural(pagination.total, "záznam", "záznamy", "záznamů")}

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

Vyčistit audit log

Smazat záznamy starší než:

Tato akce je nevratná.

)}
handleFilterChange("search", e.target.value)} /> handleFilterChange("date_from", val)} /> handleFilterChange("date_to", val)} />
{Array.from({ length: 10 }, (_, i) => (
))}
} > <> {logs.length === 0 && (
)} {logs.length > 0 && logs.map((log) => ( ))}
Čas Uživatel Akce Typ entity Popis IP

Žádné záznamy k zobrazení

{formatDatetime(log.created_at)} {log.username || "-"} {ACTION_LABELS[log.action] || log.action} {ENTITY_TYPE_LABELS[log.entity_type || ""] || log.entity_type || "-"} {log.description || "-"} {log.user_ip || "-"}
); }