import { useState, useEffect, useCallback } from 'react' 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' 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 PaginationData { total: number page: number per_page: number total_pages: number } 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 [logs, setLogs] = useState([]) const [loading, setLoading] = useState(true) const [pagination, setPagination] = useState(null) const [filters, setFilters] = useState({ search: '', action: '', entity_type: '', 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) try { 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()}`) const data = await response.json() if (data.success) { setLogs(Array.isArray(data.data) ? data.data : []) setPagination({ total: data.pagination?.total ?? 0, page: data.pagination?.page ?? 1, per_page: data.pagination?.limit ?? 50, total_pages: data.pagination?.total_pages ?? 1, }) } else { alert.error(data.error || 'Nepodařilo se načíst audit log') } } catch { alert.error('Chyba připojení') } finally { setLoading(false) } }, [filters, alert]) useEffect(() => { fetchLogs() }, [fetchLogs]) if (!hasPermission('settings.audit')) { return } const handleFilterChange = (key: keyof Filters, value: string) => { setFilters(prev => ({ ...prev, [key]: value })) } const handlePageChange = (newPage: number) => { fetchLogs(newPage, pagination?.per_page || 50) } const handlePerPageChange = (newPerPage: number) => { fetchLogs(1, 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) fetchLogs() } 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 (loading && logs.length === 0) { return (
{Array.from({ length: 8 }, (_, i) => (
))}
) } 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)} />
{loading && Array.from({ length: 10 }, (_, i) => ( ))} {!loading && logs.length === 0 && ( )} {!loading && 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 || '-'}
) }