import React, { useState, useEffect } from 'react'; import { Plus, Edit2, Trash2, ChevronDown, ChevronRight, Folder, FileText, Save, X } from 'lucide-react'; import { apiClient } from '../../services/apiClient'; interface ExpenseCategory { id: number; name: string; code?: string; description?: string; sortOrder: number; isActive: boolean; items?: ExpenseItem[]; } interface ExpenseItem { id: number; categoryId: number; name: string; code?: string; description?: string; parentItemId?: number; sortOrder: number; isActive: boolean; children?: ExpenseItem[]; } export const ExpenseDirectory: React.FC = () => { const [categories, setCategories] = useState([]); const [loading, setLoading] = useState(true); const [expandedCategories, setExpandedCategories] = useState>(new Set()); const [editingItem, setEditingItem] = useState<{ type: 'category' | 'item'; id?: number; categoryId?: number; parentId?: number } | null>(null); const [formData, setFormData] = useState({ name: '', code: '', description: '', sortOrder: 0 }); useEffect(() => { fetchDirectory(); }, []); const fetchDirectory = async () => { try { setLoading(true); const data = await apiClient.get('/api/finance/expense-directory/tree'); setCategories(data); // Раскрываем все категории по умолчанию setExpandedCategories(new Set(data.map(c => c.id))); } catch (err) { console.error('Ошибка загрузки справочника:', err); } finally { setLoading(false); } }; const handleInitialize = async () => { if (!confirm('Инициализировать справочник из стандартного списка? Существующие данные не будут удалены.')) { return; } try { await apiClient.post('/api/finance/expense-directory/initialize'); alert('Справочник успешно инициализирован'); fetchDirectory(); } catch (err: any) { alert(`Ошибка: ${err.message || 'Не удалось инициализировать справочник'}`); } }; const handleSave = async () => { if (!formData.name.trim()) { alert('Название обязательно'); return; } try { if (editingItem?.type === 'category') { if (editingItem.id) { await apiClient.put(`/api/finance/expense-categories/${editingItem.id}`, { name: formData.name, code: formData.code || null, description: formData.description || null, sortOrder: formData.sortOrder }); } else { await apiClient.post('/api/finance/expense-categories', { name: formData.name, code: formData.code || null, description: formData.description || null, sortOrder: formData.sortOrder }); } } else if (editingItem?.type === 'item') { if (!editingItem.categoryId) { alert('Не указана категория'); return; } if (editingItem.id) { await apiClient.put(`/api/finance/expense-items/${editingItem.id}`, { categoryId: editingItem.categoryId, name: formData.name, code: formData.code || null, description: formData.description || null, parentItemId: editingItem.parentId || null, sortOrder: formData.sortOrder }); } else { await apiClient.post('/api/finance/expense-items', { categoryId: editingItem.categoryId, name: formData.name, code: formData.code || null, description: formData.description || null, parentItemId: editingItem.parentId || null, sortOrder: formData.sortOrder }); } } setEditingItem(null); setFormData({ name: '', code: '', description: '', sortOrder: 0 }); fetchDirectory(); } catch (err: any) { alert(`Ошибка: ${err.message || 'Не удалось сохранить'}`); } }; const handleDelete = async (type: 'category' | 'item', id: number) => { if (!confirm(`Удалить ${type === 'category' ? 'категорию' : 'статью'}?`)) { return; } try { if (type === 'category') { await apiClient.delete(`/api/finance/expense-categories/${id}`); } else { await apiClient.delete(`/api/finance/expense-items/${id}`); } fetchDirectory(); } catch (err: any) { alert(`Ошибка: ${err.message || 'Не удалось удалить'}`); } }; const toggleCategory = (categoryId: number) => { const newExpanded = new Set(expandedCategories); if (newExpanded.has(categoryId)) { newExpanded.delete(categoryId); } else { newExpanded.add(categoryId); } setExpandedCategories(newExpanded); }; const renderItem = (item: ExpenseItem, categoryId: number, level: number = 0) => { const hasChildren = item.children && item.children.length > 0; return (
0 ? 'ml-6' : ''}`}>
{hasChildren ? ( ) : (
)} {item.name} {item.code && ( ({item.code}) )}
{hasChildren && item.children && (
{item.children.map(child => renderItem(child, categoryId, level + 1))}
)}
); }; if (loading) { return (

Загрузка справочника...

); } return (

Справочник статей расходов

{/* Форма редактирования */} {editingItem && (

{editingItem.id ? 'Редактировать' : 'Добавить'} {editingItem.type === 'category' ? 'категорию' : 'статью'}

setFormData({ ...formData, name: e.target.value })} className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent" placeholder="Введите название" />
setFormData({ ...formData, code: e.target.value })} className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent" placeholder="Код (опционально)" />
setFormData({ ...formData, sortOrder: parseInt(e.target.value) || 0 })} className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent" />