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": {
|
"require": {
|
||||||
"php": ">=8.1",
|
"php": ">=8.1",
|
||||||
"firebase/php-jwt": "^6.11",
|
"firebase/php-jwt": "^6.11",
|
||||||
"tecnickcom/tcpdf": "^6.7",
|
|
||||||
"robthree/twofactorauth": "^3.0",
|
"robthree/twofactorauth": "^3.0",
|
||||||
"chillerlan/php-qrcode": "^5.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 { useState, useEffect, useCallback, useRef } from 'react'
|
||||||
import { useAlert } from '../context/AlertContext'
|
import { useAlert } from '../context/AlertContext'
|
||||||
import apiFetch from '../utils/api'
|
import apiFetch from '../utils/api'
|
||||||
|
import useDebounce from './useDebounce'
|
||||||
|
|
||||||
const API_BASE = '/api/admin'
|
const API_BASE = '/api/admin'
|
||||||
|
|
||||||
@@ -13,50 +14,59 @@ const API_BASE = '/api/admin'
|
|||||||
* @param {string} opts.search - Hledany text
|
* @param {string} opts.search - Hledany text
|
||||||
* @param {string} opts.sort - Sloupec pro razeni
|
* @param {string} opts.sort - Sloupec pro razeni
|
||||||
* @param {string} opts.order - ASC/DESC
|
* @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
|
* @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 alert = useAlert()
|
||||||
const [items, setItems] = useState([])
|
const [items, setItems] = useState([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [pagination, setPagination] = useState(null)
|
||||||
const abortRef = useRef(null)
|
const abortRef = useRef(null)
|
||||||
const extraParamsStr = extraParams ? JSON.stringify(extraParams) : ''
|
const extraParamsStr = extraParams ? JSON.stringify(extraParams) : ''
|
||||||
|
const debouncedSearch = useDebounce(search, 300)
|
||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
const fetchData = useCallback(async () => {
|
||||||
if (abortRef.current) abortRef.current.abort()
|
if (abortRef.current) { abortRef.current.abort() }
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
abortRef.current = controller
|
abortRef.current = controller
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
if (search) params.set('search', search)
|
if (debouncedSearch) { params.set('search', debouncedSearch) }
|
||||||
if (sort) params.set('sort', sort)
|
if (sort) { params.set('sort', sort) }
|
||||||
if (order) params.set('order', order)
|
if (order) { params.set('order', order) }
|
||||||
|
if (page) { params.set('page', page) }
|
||||||
|
if (perPage) { params.set('per_page', perPage) }
|
||||||
if (extraParamsStr) {
|
if (extraParamsStr) {
|
||||||
const extra = JSON.parse(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 })
|
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()
|
const result = await response.json()
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setItems(result.data[dataKey] || [])
|
setItems(result.data[dataKey] || [])
|
||||||
|
if (result.data.pagination) {
|
||||||
|
setPagination(result.data.pagination)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || errorMsg)
|
alert.error(result.error || errorMsg)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.name === 'AbortError') return
|
if (err.name === 'AbortError') { return }
|
||||||
alert.error('Chyba připojení')
|
alert.error('Chyba připojení')
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}, [alert, endpoint, dataKey, search, sort, order, extraParamsStr, errorMsg])
|
}, [alert, endpoint, dataKey, debouncedSearch, sort, order, page, perPage, extraParamsStr, errorMsg])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData()
|
fetchData()
|
||||||
return () => { if (abortRef.current) abortRef.current.abort() }
|
return () => { if (abortRef.current) { abortRef.current.abort() } }
|
||||||
}, [fetchData])
|
}, [fetchData])
|
||||||
|
|
||||||
return { items, setItems, loading, refetch: fetchData }
|
return { items, setItems, loading, pagination, refetch: fetchData }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,14 +41,21 @@ import { defineConfig } from 'vite'
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (existsSync(vendorSrc)) {
|
if (existsSync(vendorSrc)) {
|
||||||
|
try {
|
||||||
execSync('composer install --no-dev --quiet', { cwd: resolve(__dirname) })
|
execSync('composer install --no-dev --quiet', { cwd: resolve(__dirname) })
|
||||||
|
} catch {
|
||||||
|
console.warn('⚠ composer not found, copying vendor as-is')
|
||||||
|
}
|
||||||
copyFolderSync(vendorSrc, vendorDest)
|
copyFolderSync(vendorSrc, vendorDest)
|
||||||
|
try {
|
||||||
execSync('composer install --quiet', { cwd: resolve(__dirname) })
|
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) {
|
} catch (err) {
|
||||||
console.error('Error copying vendor folder:', err)
|
console.error('Error copying vendor folder:', err)
|
||||||
execSync('composer install --quiet', { cwd: resolve(__dirname) })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('✓ Build complete!')
|
console.log('✓ Build complete!')
|
||||||
|
|||||||
Reference in New Issue
Block a user