style: run prettier on entire codebase

This commit is contained in:
BOHA
2026-03-24 19:59:14 +01:00
parent 872be42107
commit 3c167cf5c4
148 changed files with 26740 additions and 13990 deletions

View File

@@ -1,220 +1,263 @@
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'
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 API_BASE = "/api/admin";
const ACTION_LABELS: Record<string, string> = {
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',
}
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<string, string> = {
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',
}
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<string, string> = {
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',
}
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 }))
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
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
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
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<AuditLogEntry[]>([])
const [loading, setLoading] = useState(true)
const [pagination, setPagination] = useState<PaginationData | null>(null)
const { hasPermission } = useAuth();
const alert = useAlert();
const [logs, setLogs] = useState<AuditLogEntry[]>([]);
const [loading, setLoading] = useState(true);
const [pagination, setPagination] = useState<PaginationData | null>(null);
const [filters, setFilters] = useState<Filters>({
search: '',
action: '',
entity_type: '',
date_from: '',
date_to: '',
})
const [showCleanup, setShowCleanup] = useState(false)
const [cleanupDays, setCleanupDays] = useState(90)
const [cleaning, setCleaning] = useState(false)
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) })
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)
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()
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')
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);
}
} catch {
alert.error('Chyba připojení')
} finally {
setLoading(false)
}
}, [filters, alert])
},
[filters, alert],
);
useEffect(() => {
fetchLogs()
}, [fetchLogs])
fetchLogs();
}, [fetchLogs]);
if (!hasPermission('settings.audit')) {
return <Forbidden />
if (!hasPermission("settings.audit")) {
return <Forbidden />;
}
const handleFilterChange = (key: keyof Filters, value: string) => {
setFilters(prev => ({ ...prev, [key]: value }))
}
setFilters((prev) => ({ ...prev, [key]: value }));
};
const handlePageChange = (newPage: number) => {
fetchLogs(newPage, pagination?.per_page || 50)
}
fetchLogs(newPage, pagination?.per_page || 50);
};
const handlePerPageChange = (newPerPage: number) => {
fetchLogs(1, newPerPage)
}
fetchLogs(1, newPerPage);
};
const handleCleanup = async () => {
setCleaning(true)
setCleaning(true);
try {
const response = await apiFetch(`${API_BASE}/audit-log/cleanup`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ days: cleanupDays }),
})
const data = await response.json()
});
const data = await response.json();
if (data.success) {
alert.success(data.message)
setShowCleanup(false)
fetchLogs()
alert.success(data.message);
setShowCleanup(false);
fetchLogs();
} else {
alert.error(data.error)
alert.error(data.error);
}
} catch {
alert.error('Chyba připojení')
alert.error("Chyba připojení");
} finally {
setCleaning(false)
setCleaning(false);
}
}
};
const formatDatetime = (dateString: string | null): string => {
if (!dateString) return '-'
return new Date(dateString).toLocaleString('cs-CZ')
}
if (!dateString) return "-";
return new Date(dateString).toLocaleString("cs-CZ");
};
if (loading && logs.length === 0) {
return (
<div className="admin-skeleton" style={{ padding: 0, gap: '1.5rem' }}>
<div className="admin-skeleton-row" style={{ justifyContent: 'space-between' }}>
<div className="admin-skeleton" style={{ padding: 0, gap: "1.5rem" }}>
<div
className="admin-skeleton-row"
style={{ justifyContent: "space-between" }}
>
<div>
<div className="admin-skeleton-line h-8" style={{ width: '160px', marginBottom: '0.5rem' }} />
<div className="admin-skeleton-line" style={{ width: '100px' }} />
<div
className="admin-skeleton-line h-8"
style={{ width: "160px", marginBottom: "0.5rem" }}
/>
<div className="admin-skeleton-line" style={{ width: "100px" }} />
</div>
</div>
<div className="admin-card">
<div className="admin-skeleton" style={{ gap: '0.75rem', padding: '1rem' }}>
<div className="admin-skeleton-line h-10" style={{ width: '100%', borderRadius: '8px' }} />
<div
className="admin-skeleton"
style={{ gap: "0.75rem", padding: "1rem" }}
>
<div
className="admin-skeleton-line h-10"
style={{ width: "100%", borderRadius: "8px" }}
/>
</div>
</div>
<div className="admin-card">
<div className="admin-skeleton" style={{ gap: '1rem' }}>
<div className="admin-skeleton-line h-10" style={{ width: '100%', borderRadius: '4px' }} />
<div className="admin-skeleton" style={{ gap: "1rem" }}>
<div
className="admin-skeleton-line h-10"
style={{ width: "100%", borderRadius: "4px" }}
/>
{Array.from({ length: 8 }, (_, i) => (
<div key={i} className="admin-skeleton-row">
<div className="admin-skeleton-line" style={{ width: '120px' }} />
<div className="admin-skeleton-line" style={{ width: '80px' }} />
<div className="admin-skeleton-line" style={{ width: '70px', borderRadius: '10px' }} />
<div className="admin-skeleton-line" style={{ width: '80px' }} />
<div
className="admin-skeleton-line"
style={{ width: "120px" }}
/>
<div
className="admin-skeleton-line"
style={{ width: "80px" }}
/>
<div
className="admin-skeleton-line"
style={{ width: "70px", borderRadius: "10px" }}
/>
<div
className="admin-skeleton-line"
style={{ width: "80px" }}
/>
<div className="admin-skeleton-line flex-1" />
<div className="admin-skeleton-line" style={{ width: '90px' }} />
<div
className="admin-skeleton-line"
style={{ width: "90px" }}
/>
</div>
))}
</div>
</div>
</div>
)
);
}
return (
@@ -229,7 +272,8 @@ export default function AuditLog() {
<h1 className="admin-page-title">Audit log</h1>
{pagination && (
<p className="admin-page-subtitle">
{pagination.total} {czechPlural(pagination.total, 'záznam', 'záznamy', 'záznamů')}
{pagination.total}{" "}
{czechPlural(pagination.total, "záznam", "záznamy", "záznamů")}
</p>
)}
</div>
@@ -237,7 +281,14 @@ export default function AuditLog() {
className="admin-btn admin-btn-secondary admin-btn-sm"
onClick={() => setShowCleanup(true)}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<polyline points="3 6 5 6 21 6" />
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
</svg>
@@ -247,7 +298,10 @@ export default function AuditLog() {
{showCleanup && (
<div className="admin-modal-overlay" style={{ opacity: 1 }}>
<div className="admin-modal-backdrop" onClick={() => !cleaning && setShowCleanup(false)} />
<div
className="admin-modal-backdrop"
onClick={() => !cleaning && setShowCleanup(false)}
/>
<motion.div
className="admin-modal admin-confirm-modal"
initial={{ opacity: 0, scale: 0.95, y: 20 }}
@@ -256,14 +310,23 @@ export default function AuditLog() {
>
<div className="admin-modal-body admin-confirm-content">
<div className="admin-confirm-icon admin-confirm-icon-danger">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<polyline points="3 6 5 6 21 6" />
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
</svg>
</div>
<h2 className="admin-confirm-title">Vyčistit audit log</h2>
<p className="admin-confirm-message">Smazat záznamy starší než:</p>
<div style={{ margin: '0.75rem auto', maxWidth: '200px' }}>
<p className="admin-confirm-message">
Smazat záznamy starší než:
</p>
<div style={{ margin: "0.75rem auto", maxWidth: "200px" }}>
<select
className="admin-form-select"
value={cleanupDays}
@@ -277,7 +340,12 @@ export default function AuditLog() {
<option value={0}>Vše</option>
</select>
</div>
<p className="admin-confirm-message" style={{ fontSize: '12px', opacity: 0.6 }}>Tato akce je nevratná.</p>
<p
className="admin-confirm-message"
style={{ fontSize: "12px", opacity: 0.6 }}
>
Tato akce je nevratná.
</p>
</div>
<div className="admin-modal-footer">
<button
@@ -294,7 +362,7 @@ export default function AuditLog() {
className="admin-btn admin-btn-primary"
disabled={cleaning}
>
{cleaning ? 'Mažu...' : 'Smazat'}
{cleaning ? "Mažu..." : "Smazat"}
</button>
</div>
</motion.div>
@@ -315,18 +383,20 @@ export default function AuditLog() {
className="admin-form-input"
placeholder="Popis, uživatel..."
value={filters.search}
onChange={(e) => handleFilterChange('search', e.target.value)}
onChange={(e) => handleFilterChange("search", e.target.value)}
/>
</FormField>
<FormField label="Akce">
<select
className="admin-form-select"
value={filters.action}
onChange={(e) => handleFilterChange('action', e.target.value)}
onChange={(e) => handleFilterChange("action", e.target.value)}
>
<option value="">Všechny</option>
{ACTION_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
{ACTION_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</FormField>
@@ -334,11 +404,15 @@ export default function AuditLog() {
<select
className="admin-form-select"
value={filters.entity_type}
onChange={(e) => handleFilterChange('entity_type', e.target.value)}
onChange={(e) =>
handleFilterChange("entity_type", e.target.value)
}
>
<option value="">Všechny</option>
{ENTITY_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
{ENTITY_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</FormField>
@@ -346,14 +420,14 @@ export default function AuditLog() {
<AdminDatePicker
mode="date"
value={filters.date_from}
onChange={(val: string) => handleFilterChange('date_from', val)}
onChange={(val: string) => handleFilterChange("date_from", val)}
/>
</FormField>
<FormField label="Do">
<AdminDatePicker
mode="date"
value={filters.date_to}
onChange={(val: string) => handleFilterChange('date_to', val)}
onChange={(val: string) => handleFilterChange("date_to", val)}
/>
</FormField>
</div>
@@ -380,22 +454,64 @@ export default function AuditLog() {
</tr>
</thead>
<tbody>
{loading && Array.from({ length: 10 }, (_, i) => (
<tr key={`skeleton-${i}`}>
<td><div className="admin-skeleton-line" style={{ width: '110px', height: '14px' }} /></td>
<td><div className="admin-skeleton-line" style={{ width: '80px', height: '14px' }} /></td>
<td><div className="admin-skeleton-line" style={{ width: '70px', height: '22px', borderRadius: '10px' }} /></td>
<td><div className="admin-skeleton-line" style={{ width: '80px', height: '14px' }} /></td>
<td><div className="admin-skeleton-line" style={{ width: '60%', height: '14px' }} /></td>
<td><div className="admin-skeleton-line" style={{ width: '90px', height: '14px' }} /></td>
</tr>
))}
{loading &&
Array.from({ length: 10 }, (_, i) => (
<tr key={`skeleton-${i}`}>
<td>
<div
className="admin-skeleton-line"
style={{ width: "110px", height: "14px" }}
/>
</td>
<td>
<div
className="admin-skeleton-line"
style={{ width: "80px", height: "14px" }}
/>
</td>
<td>
<div
className="admin-skeleton-line"
style={{
width: "70px",
height: "22px",
borderRadius: "10px",
}}
/>
</td>
<td>
<div
className="admin-skeleton-line"
style={{ width: "80px", height: "14px" }}
/>
</td>
<td>
<div
className="admin-skeleton-line"
style={{ width: "60%", height: "14px" }}
/>
</td>
<td>
<div
className="admin-skeleton-line"
style={{ width: "90px", height: "14px" }}
/>
</td>
</tr>
))}
{!loading && logs.length === 0 && (
<tr>
<td colSpan={6}>
<div className="admin-empty-state">
<div className="admin-empty-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<svg
width="28"
height="28"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
<line x1="16" y1="13" x2="8" y2="13" />
@@ -407,20 +523,29 @@ export default function AuditLog() {
</td>
</tr>
)}
{!loading && logs.map((log) => (
<tr key={log.id}>
<td className="admin-mono">{formatDatetime(log.created_at)}</td>
<td className="fw-500">{log.username || '-'}</td>
<td>
<span className={`admin-badge ${ACTION_BADGE_CLASS[log.action] || 'admin-badge-secondary'}`}>
{ACTION_LABELS[log.action] || log.action}
</span>
</td>
<td>{ENTITY_TYPE_LABELS[log.entity_type || ''] || log.entity_type || '-'}</td>
<td>{log.description || '-'}</td>
<td className="admin-mono">{log.user_ip || '-'}</td>
</tr>
))}
{!loading &&
logs.map((log) => (
<tr key={log.id}>
<td className="admin-mono">
{formatDatetime(log.created_at)}
</td>
<td className="fw-500">{log.username || "-"}</td>
<td>
<span
className={`admin-badge ${ACTION_BADGE_CLASS[log.action] || "admin-badge-secondary"}`}
>
{ACTION_LABELS[log.action] || log.action}
</span>
</td>
<td>
{ENTITY_TYPE_LABELS[log.entity_type || ""] ||
log.entity_type ||
"-"}
</td>
<td>{log.description || "-"}</td>
<td className="admin-mono">{log.user_ip || "-"}</td>
</tr>
))}
</tbody>
</table>
</div>
@@ -433,5 +558,5 @@ export default function AuditLog() {
</div>
</motion.div>
</div>
)
);
}