import { useState } from "react"; import { useQuery } 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 FormModal from "../components/FormModal"; import AdminDatePicker from "../components/AdminDatePicker"; import { czechPlural } from "../utils/formatters"; import apiFetch from "../utils/api"; import { useApiMutation } from "../lib/queries/mutations"; import { ENTITY_TYPE_LABELS } from "../lib/entityTypeLabels"; 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 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 [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 { 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: AuditLogEntry[] = logsData?.data ?? []; const pagination = logsData?.pagination ?? null; if (!hasPermission("settings.audit")) { return ; } const cleanupMutation = useApiMutation< { days: number; confirm?: string }, { message?: string; error?: string } >({ url: () => `${API_BASE}/audit-log/cleanup`, method: () => "POST", invalidate: ["audit-log"], onSuccess: (data) => { alert.success(data?.message || "Hotovo"); setShowCleanup(false); }, }); 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 () => { try { // Backend requires this literal confirmation token when wiping everything. await cleanupMutation.mutateAsync({ days: cleanupDays, ...(cleanupDays === 0 ? { confirm: "DELETE_ALL_AUDIT" } : {}), }); } catch (e) { alert.error(e instanceof Error ? e.message : "Chyba připojení"); } }; 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ů")}

)}
!cleanupMutation.isPending && setShowCleanup(false)} onSubmit={handleCleanup} title="Vyčistit audit log" submitLabel="Smazat" loading={cleanupMutation.isPending} >

Smazat záznamy starší než:

Tato akce je nevratná.

handleFilterChange("search", e.target.value)} /> handleFilterChange("date_from", val)} /> handleFilterChange("date_to", val)} />
{isPending ? (
) : ( {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 || "-"}
)}
); }