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:
@@ -5,7 +5,6 @@
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"firebase/php-jwt": "^6.11",
|
||||
"tecnickcom/tcpdf": "^6.7",
|
||||
"robthree/twofactorauth": "^3.0",
|
||||
"chillerlan/php-qrcode": "^5.0"
|
||||
},
|
||||
|
||||
20
migrations/001_add_indexes.sql
Normal file
20
migrations/001_add_indexes.sql
Normal file
@@ -0,0 +1,20 @@
|
||||
-- Indexy pro casto filtrovane sloupce
|
||||
-- Spustit rucne: mysql -u root app < migrations/001_add_indexes.sql
|
||||
|
||||
-- Dochazka - filtrovani dle uzivatele a data smeny
|
||||
CREATE INDEX idx_attendance_user_date ON attendance(user_id, shift_date);
|
||||
|
||||
-- Vydane faktury - filtrovani dle statusu a data vystaveni (KPI karty, seznamy)
|
||||
CREATE INDEX idx_invoices_status_issue ON invoices(status, issue_date);
|
||||
|
||||
-- Vydane faktury - detekce po splatnosti
|
||||
CREATE INDEX idx_invoices_due_date ON invoices(due_date);
|
||||
|
||||
-- Nabidky - razeni a vyhledavani dle cisla
|
||||
CREATE INDEX idx_quotations_number ON quotations(quotation_number);
|
||||
|
||||
-- Refresh tokeny - cleanup a validace dle uzivatele a expirace
|
||||
CREATE INDEX idx_refresh_tokens_user_exp ON refresh_tokens(user_id, expires_at);
|
||||
|
||||
-- Audit log - razeni a mazani starych zaznamu
|
||||
CREATE INDEX idx_audit_log_created ON audit_log(created_at);
|
||||
16
src/admin/hooks/useDebounce.js
Normal file
16
src/admin/hooks/useDebounce.js
Normal 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
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -41,14 +41,21 @@ import { defineConfig } from 'vite'
|
||||
|
||||
try {
|
||||
if (existsSync(vendorSrc)) {
|
||||
try {
|
||||
execSync('composer install --no-dev --quiet', { cwd: resolve(__dirname) })
|
||||
} catch {
|
||||
console.warn('⚠ composer not found, copying vendor as-is')
|
||||
}
|
||||
copyFolderSync(vendorSrc, vendorDest)
|
||||
try {
|
||||
execSync('composer install --quiet', { cwd: resolve(__dirname) })
|
||||
console.log('✓ Vendor folder copied to dist/vendor (production only)')
|
||||
} catch {
|
||||
// composer not in PATH - dev deps already present
|
||||
}
|
||||
console.log('✓ Vendor folder copied to dist/vendor')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error copying vendor folder:', err)
|
||||
execSync('composer install --quiet', { cwd: resolve(__dirname) })
|
||||
}
|
||||
|
||||
console.log('✓ Build complete!')
|
||||
|
||||
Reference in New Issue
Block a user