From 6ad20ea04e87f270231c9be5f89616e7ede5b96d Mon Sep 17 00:00:00 2001 From: Simon Date: Thu, 12 Mar 2026 17:57:02 +0100 Subject: [PATCH] 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 --- composer.json | 1 - migrations/001_add_indexes.sql | 20 ++++++++++++++++++++ src/admin/hooks/useDebounce.js | 16 ++++++++++++++++ src/admin/hooks/useListData.js | 32 +++++++++++++++++++++----------- vite.config.js | 15 +++++++++++---- 5 files changed, 68 insertions(+), 16 deletions(-) create mode 100644 migrations/001_add_indexes.sql create mode 100644 src/admin/hooks/useDebounce.js diff --git a/composer.json b/composer.json index 84f69d8..e7b89ed 100644 --- a/composer.json +++ b/composer.json @@ -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" }, diff --git a/migrations/001_add_indexes.sql b/migrations/001_add_indexes.sql new file mode 100644 index 0000000..1754250 --- /dev/null +++ b/migrations/001_add_indexes.sql @@ -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); diff --git a/src/admin/hooks/useDebounce.js b/src/admin/hooks/useDebounce.js new file mode 100644 index 0000000..f6a81b6 --- /dev/null +++ b/src/admin/hooks/useDebounce.js @@ -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 +} diff --git a/src/admin/hooks/useListData.js b/src/admin/hooks/useListData.js index 9ceb9d2..da519f8 100644 --- a/src/admin/hooks/useListData.js +++ b/src/admin/hooks/useListData.js @@ -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 } } diff --git a/vite.config.js b/vite.config.js index 8b44012..ed0b453 100644 --- a/vite.config.js +++ b/vite.config.js @@ -41,14 +41,21 @@ import { defineConfig } from 'vite' try { if (existsSync(vendorSrc)) { - execSync('composer install --no-dev --quiet', { cwd: resolve(__dirname) }) + try { + execSync('composer install --no-dev --quiet', { cwd: resolve(__dirname) }) + } catch { + console.warn('⚠ composer not found, copying vendor as-is') + } copyFolderSync(vendorSrc, vendorDest) - execSync('composer install --quiet', { cwd: resolve(__dirname) }) - console.log('✓ Vendor folder copied to dist/vendor (production only)') + try { + execSync('composer install --quiet', { cwd: resolve(__dirname) }) + } 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!')