/
Pitonx
/
MyFinance
Обзор
Документация
Войти
/
Pitonx
/
MyFinance
Код
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/components/SearchForm.js
158 строк
6 KB
Сергей
v9.7.2-9.7.12. Вошедшие изменения см. в CHANGELOG.md
09 май 2026, 16:20
09 май 2026, 16:20
92cbce6
Код
Авторство
О чём код?
import React, { useState, useCallback, useEffect } from 'react'; import { SEARCH_FIELDS, ICONS } from '../config/entities'; import { useLookupData } from '../hooks/useLookupData'; import TagSelector from './TagSelector'; function SelectField({ field, value, onChange, lookups, filters }) { const { accounts, currencies, expenseCategories, expenseSubcategories, incomeCategories, incomeSubcategories, creditors, debtors, tags, } = lookups; const getOptions = () => { switch (field.source) { case 'accounts': return accounts.filter(a => !a.hidden); case 'currencies': return []; case 'expenseCategories': return expenseCategories; case 'expenseSubcategories': return field.dependsOn && filters?.[field.dependsOn] ? expenseSubcategories.filter(s => String(s.categoryId ?? s.category_id ?? s.category ?? '') === String(filters[field.dependsOn] ?? '') ) : expenseSubcategories; case 'incomeCategories': return incomeCategories; case 'incomeSubcategories': return field.dependsOn && filters?.[field.dependsOn] ? incomeSubcategories.filter(s => String(s.categoryId ?? s.category_id ?? s.category ?? '') === String(filters[field.dependsOn] ?? '') ) : incomeSubcategories; case 'creditors': return creditors; case 'debtors': return debtors; case 'tags': return tags; default: return field.options || []; } }; const options = getOptions(); return ( <div className="form-group"> <label>{field.label}</label> <select value={value || ''} onChange={e => onChange(field.name, e.target.value)}> <option value="">Все</option> {options.map(opt => { const label = field.source === 'accounts' ? (() => { const curr = currencies.find(c => c.id === opt.currencyId); return curr ? `${opt.name} (${curr.code})` : opt.name; })() : (opt.name || opt.label || opt.id || ''); return ( <option key={opt.id || opt.value || opt.id} value={opt.id || opt.value || opt.id}> {label} </option> ); })} </select> </div> ); } function MultiselectField({ field, value, onChange, lookups }) { return ( <TagSelector options={lookups[field.source] || []} value={value || []} onChange={(newIds) => onChange(field.name, newIds)} label={field.label} /> ); } export default function SearchForm({ entity, onSearch, filters: externalFilters = {} }) { const [filters, setFilters] = useState(externalFilters); const [isMobile, setIsMobile] = useState(() => window.matchMedia('(max-width: 768px)').matches); const [collapsed, setCollapsed] = useState(false); const lookups = useLookupData(); // Отслеживаем мобильный режим useEffect(() => { const mq = window.matchMedia('(max-width: 768px)'); const handler = (e) => setIsMobile(e.matches); mq.addEventListener('change', handler); return () => mq.removeEventListener('change', handler); }, []); // Синхронизация с внешними фильтрами useEffect(() => { setFilters(externalFilters || {}); }, [externalFilters]); const fields = SEARCH_FIELDS[entity] || []; const handleChange = useCallback((name, value) => { setFilters(prev => ({ ...prev, [name]: value })); }, []); const handleSubmit = (e) => { e.preventDefault(); onSearch(filters); if (isMobile) setCollapsed(true); }; const handleReset = () => { setFilters({}); onSearch({}); if (isMobile) setCollapsed(true); }; if (fields.length === 0) return null; const hasActiveFilters = Object.keys(filters).some(k => filters[k] != null && filters[k] !== ''); return ( <form className="search-panel" onSubmit={handleSubmit}> {isMobile && collapsed ? ( <div style={{ display: 'flex', justifyContent: 'center', padding: '4px 0' }}> <button type="button" className="btn btn-sm" onClick={() => setCollapsed(false)}> {ICONS.search} Показать фильтры </button> </div> ) : ( <> {isMobile && ( <div style={{ display: 'flex', justifyContent: 'flex-end', padding: '0 0 4px' }}> <button type="button" className="btn btn-sm" onClick={() => setCollapsed(true)}> Скрыть </button> </div> )} {fields.map(field => { const value = filters[field.name]; if (field.type === 'select') { return <SelectField key={field.name} field={field} value={value} onChange={handleChange} lookups={lookups} filters={filters} />; } if (field.type === 'multiselect') { return <MultiselectField key={field.name} field={field} value={value} onChange={handleChange} lookups={lookups} />; } return ( <div key={field.name} className="form-group"> <label>{field.label}</label> <input type={field.type} value={value || ''} onChange={e => handleChange(field.name, e.target.value)} /> </div> ); })} <div style={{ display: 'flex', alignItems: 'flex-end', gap: 6, flexShrink: 0 }}> <button type="submit" className="btn btn-primary">{ICONS.search}</button> <button type="button" className="btn" onClick={handleReset}>{ICONS.refresh}</button> </div> </> )} </form> ); }