feat: P1 quick wins - useDebounce hook, DB indexy, odstraneni TCPDF

- useDebounce hook (300ms) integrovan do useListData pro debounce hledani
- useListData rozsiren o page/perPage/pagination parametry (priprava pro P2)
- Migracni SQL s indexy na attendance, invoices, quotations, refresh_tokens, audit_log
- Odstranen nepouzivany TCPDF z composer.json
- Vite build plugin: graceful handling kdyz composer neni v PATH

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 17:57:02 +01:00
parent bb2bbb8ff6
commit 6ad20ea04e
5 changed files with 68 additions and 16 deletions

View File

@@ -0,0 +1,16 @@
import { useState, useEffect } from 'react'
/**
* Debounce hook - zpozdi zmenu hodnoty o zadany cas.
* Pouziti: const debouncedSearch = useDebounce(search, 300)
*/
export default function useDebounce(value, delay = 300) {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay)
return () => clearTimeout(timer)
}, [value, delay])
return debouncedValue
}

View File

@@ -1,6 +1,7 @@
import { useState, useEffect, useCallback, useRef } from 'react'
import { useAlert } from '../context/AlertContext'
import apiFetch from '../utils/api'
import useDebounce from './useDebounce'
const API_BASE = '/api/admin'
@@ -13,50 +14,59 @@ const API_BASE = '/api/admin'
* @param {string} opts.search - Hledany text
* @param {string} opts.sort - Sloupec pro razeni
* @param {string} opts.order - ASC/DESC
* @param {number} [opts.page] - Cislo stranky (1-based)
* @param {number} [opts.perPage] - Pocet zaznamu na stranku
* @param {string} [opts.errorMsg] - Chybova zprava pri neuspechu
*/
export default function useListData(endpoint, { dataKey, search, sort, order, extraParams, errorMsg = 'Nepodařilo se načíst data' } = {}) {
export default function useListData(endpoint, { dataKey, search, sort, order, page, perPage, extraParams, errorMsg = 'Nepodařilo se načíst data' } = {}) {
const alert = useAlert()
const [items, setItems] = useState([])
const [loading, setLoading] = useState(true)
const [pagination, setPagination] = useState(null)
const abortRef = useRef(null)
const extraParamsStr = extraParams ? JSON.stringify(extraParams) : ''
const debouncedSearch = useDebounce(search, 300)
const fetchData = useCallback(async () => {
if (abortRef.current) abortRef.current.abort()
if (abortRef.current) { abortRef.current.abort() }
const controller = new AbortController()
abortRef.current = controller
try {
const params = new URLSearchParams()
if (search) params.set('search', search)
if (sort) params.set('sort', sort)
if (order) params.set('order', order)
if (debouncedSearch) { params.set('search', debouncedSearch) }
if (sort) { params.set('sort', sort) }
if (order) { params.set('order', order) }
if (page) { params.set('page', page) }
if (perPage) { params.set('per_page', perPage) }
if (extraParamsStr) {
const extra = JSON.parse(extraParamsStr)
Object.entries(extra).forEach(([k, v]) => { if (v) params.set(k, v) })
Object.entries(extra).forEach(([k, v]) => { if (v) { params.set(k, v) } })
}
const response = await apiFetch(`${API_BASE}/${endpoint}?${params}`, { signal: controller.signal })
if (response.status === 401) return
if (response.status === 401) { return }
const result = await response.json()
if (result.success) {
setItems(result.data[dataKey] || [])
if (result.data.pagination) {
setPagination(result.data.pagination)
}
} else {
alert.error(result.error || errorMsg)
}
} catch (err) {
if (err.name === 'AbortError') return
if (err.name === 'AbortError') { return }
alert.error('Chyba připojení')
} finally {
setLoading(false)
}
}, [alert, endpoint, dataKey, search, sort, order, extraParamsStr, errorMsg])
}, [alert, endpoint, dataKey, debouncedSearch, sort, order, page, perPage, extraParamsStr, errorMsg])
useEffect(() => {
fetchData()
return () => { if (abortRef.current) abortRef.current.abort() }
return () => { if (abortRef.current) { abortRef.current.abort() } }
}, [fetchData])
return { items, setItems, loading, refetch: fetchData }
return { items, setItems, loading, pagination, refetch: fetchData }
}