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 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 20:48:05 +01:00
parent 36a864c852
commit 5529219234
5 changed files with 157 additions and 17 deletions

View File

@@ -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 (
<div className="admin-card dash-activity-card">
<div className="admin-card-header">
<h2 className="admin-card-title">Poslední aktivita</h2>
<div className="admin-card-header" style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<h2 className="admin-card-title">Audit log</h2>
<Link to="/audit-log" className="admin-btn admin-btn-primary admin-btn-sm">Detail &rarr;</Link>
</div>
<div className="admin-card-body" style={{ padding: 0 }}>
{activities.map((act) => (

View File

@@ -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() {
</p>
)}
</div>
<button
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">
<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>
Vyčistit
</button>
</motion.div>
{showCleanup && (
<div className="admin-modal-overlay" style={{ opacity: 1 }}>
<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 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
transition={{ duration: 0.2 }}
>
<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">
<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' }}>
<select
className="admin-form-select"
value={cleanupDays}
onChange={(e) => setCleanupDays(parseInt(e.target.value))}
>
<option value={30}>30 dní</option>
<option value={60}>60 dní</option>
<option value={90}>90 dní</option>
<option value={180}>180 dní</option>
<option value={365}>1 rok</option>
<option value={0}>Vše</option>
</select>
</div>
<p className="admin-confirm-message" style={{ fontSize: '12px', opacity: 0.6 }}>Tato akce je nevratná.</p>
</div>
<div className="admin-modal-footer">
<button
type="button"
onClick={() => setShowCleanup(false)}
className="admin-btn admin-btn-secondary"
disabled={cleaning}
>
Zrušit
</button>
<button
type="button"
onClick={handleCleanup}
className="admin-btn admin-btn-primary"
disabled={cleaning}
>
{cleaning ? 'Mažu...' : 'Smazat'}
</button>
</div>
</motion.div>
</div>
)}
<motion.div
className="admin-card"
initial={{ opacity: 0, y: 20 }}
@@ -205,7 +297,7 @@ export default function AuditLog() {
style={{ marginBottom: '1rem' }}
>
<div className="admin-card-body">
<div className="admin-form-row" style={{ gridTemplateColumns: '2fr 1fr 1fr' }}>
<div className="admin-form-row" style={{ gridTemplateColumns: '1.2fr 1fr 1fr 1fr 1fr' }}>
<FormField label="Hledat">
<input
type="text"
@@ -239,8 +331,6 @@ export default function AuditLog() {
))}
</select>
</FormField>
</div>
<div className="admin-form-row" style={{ gridTemplateColumns: '1fr 1fr', maxWidth: '400px', marginTop: '0.75rem' }}>
<FormField label="Od">
<AdminDatePicker
mode="date"