import 'package:flutter/material.dart'; import 'package:drift/drift.dart' show Value; import '../database/database.dart' as db; import '../utils/category_utils.dart'; class EditCategoryDialog extends StatefulWidget { final db.AppDatabase database; final db.CategoryDb? categoryToEdit; // null если добавляем новую const EditCategoryDialog({ Key? key, required this.database, this.categoryToEdit, }) : super(key: key); @override State createState() => _EditCategoryDialogState(); } class _EditCategoryDialogState extends State { final _formKey = GlobalKey(); late TextEditingController _nameController; String? _selectedIconName; Color? _selectedColor; bool get _isEditing => widget.categoryToEdit != null; @override void initState() { super.initState(); _nameController = TextEditingController(text: widget.categoryToEdit?.name ?? ''); // Ensure the initial icon exists in the available list, otherwise pick the first _selectedIconName = widget.categoryToEdit?.icon; if (_selectedIconName == null || !CategoryUtils.getAvailableIcons().containsKey(_selectedIconName)) { _selectedIconName = CategoryUtils.getAvailableIcons().keys.first; } // Инициализируем цвет. // Если редактируем существующую категорию, используем ее цвет из БД. // Если добавляем новую, используем первый цвет из доступных. if (_isEditing) { _selectedColor = Color(widget.categoryToEdit!.color); } else { _selectedColor = CategoryUtils.availableColors.first; } // Убедимся, что _selectedColor не null после инициализации // (это должно быть гарантировано логикой выше, но для безопасности) _selectedColor ??= CategoryUtils.availableColors.first; } @override void dispose() { _nameController.dispose(); super.dispose(); } Future _saveCategory() async { if (_formKey.currentState!.validate()) { final name = _nameController.text.trim(); final icon = _selectedIconName; final color = _selectedColor; if (icon == null || color == null) { // This should not happen due to initialization logic, but check anyway if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Пожалуйста, выберите иконку и цвет'), backgroundColor: Colors.orange, behavior: SnackBarBehavior.floating, ), ); } return; } // Проверка на уникальность имени (кроме случая редактирования той же категории) // Используем case-insensitive сравнение final existingCategories = await widget.database.watchAllCategoriesDb().first; final isNameTaken = existingCategories.any((c) => c.name.toLowerCase() == name.toLowerCase() && (!_isEditing || c.id != widget.categoryToEdit!.id)); // Проверяем ID только при редактировании if (isNameTaken) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Категория с именем "$name" уже существует.'), backgroundColor: Colors.orange, behavior: SnackBarBehavior.floating, ), ); } return; } // Запрещаем имя 'Income' (case-insensitive) if (name.toLowerCase() == 'income') { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Имя "Income" зарезервировано.'), backgroundColor: Colors.orange, behavior: SnackBarBehavior.floating, ), ); } return; } final companion = db.CategoriesCompanion( id: _isEditing ? Value(widget.categoryToEdit!.id) : const Value.absent(), name: Value(name), icon: Value(icon), color: Value(color.value), ); try { if (_isEditing) { await widget.database.updateCategory(companion); } else { await widget.database.addCategory(companion); } if (mounted) Navigator.of(context).pop(true); // Возвращаем true при успехе } catch (e) { print('Error saving category: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Ошибка сохранения категории: ${e.toString()}'), backgroundColor: Colors.red, behavior: SnackBarBehavior.floating, ), ); } } } } @override Widget build(BuildContext context) { final theme = Theme.of(context); final isDark = theme.brightness == Brightness.dark; final availableIcons = CategoryUtils.getAvailableIcons(); // Get filtered icons // Объединяем цвет текущей категории (если редактируем) с доступными цветами // для отображения в палитре. Это нужно, чтобы текущий цвет был виден, // даже если его нет в стандартном списке. final List displayedColors = List.from(CategoryUtils.availableColors); if (_isEditing && _selectedColor != null && !CategoryUtils.availableColors.contains(_selectedColor)) { // Добавляем цвет текущей категории в начало списка для отображения displayedColors.insert(0, _selectedColor!); } return AlertDialog( title: Text(_isEditing ? 'Редактировать категорию' : 'Добавить категорию'), contentPadding: const EdgeInsets.fromLTRB(24.0, 20.0, 24.0, 0.0), // Adjust padding content: SizedBox( // Constrain width for better appearance on large screens width: MediaQuery.of(context).size.width * 0.8, // Example width constraint child: SingleChildScrollView( // Позволяет прокручивать, если не помещается child: Form( key: _formKey, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, // Align labels to start children: [ // --- Поле Имя --- TextFormField( controller: _nameController, decoration: InputDecoration( labelText: 'Название категории', border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), filled: true, fillColor: isDark ? Colors.grey.shade800.withOpacity(0.5) : Colors.grey.shade100, ), validator: (value) { if (value == null || value.trim().isEmpty) { return 'Введите название'; } if (value.trim().toLowerCase() == 'income') { return 'Имя "Income" зарезервировано'; } return null; }, textCapitalization: TextCapitalization.words, ), const SizedBox(height: 20), // --- Выбор Иконки --- DropdownButtonFormField( value: _selectedIconName, isExpanded: true, // Allow dropdown to expand decoration: InputDecoration( labelText: 'Иконка', border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), filled: true, fillColor: isDark ? Colors.grey.shade800.withOpacity(0.5) : Colors.grey.shade100, contentPadding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0), ), items: availableIcons.entries.map((entry) { return DropdownMenuItem( value: entry.key, child: Row( children: [ Icon(entry.value, color: _selectedColor ?? theme.colorScheme.primary, size: 20), const SizedBox(width: 12), // Отображаем имя иконки, убирая '_outlined' и делая первую букву заглавной Text(entry.key.replaceAll('_outlined', '').replaceAll('_', ' ').capitalizeFirst()), ], ), ); }).toList(), onChanged: (value) { if (value != null) { setState(() { _selectedIconName = value; }); } }, validator: (value) => value == null ? 'Выберите иконку' : null, ), const SizedBox(height: 20), // --- Выбор Цвета --- Text('Цвет категории:', style: theme.textTheme.titleSmall), const SizedBox(height: 10), Wrap( // Используем Wrap для отображения цветов в несколько рядов spacing: 10.0, // Горизонтальный отступ runSpacing: 10.0, // Вертикальный отступ children: displayedColors.map((color) { // Используем объединенный список цветов final isSelected = _selectedColor == color; return GestureDetector( onTap: () { setState(() { _selectedColor = color; }); }, child: Container( width: 38, height: 38, decoration: BoxDecoration( color: color, shape: BoxShape.circle, border: Border.all( color: isSelected ? (isDark ? Colors.white70 : Colors.black87) : theme.dividerColor.withOpacity(0.5), // Subtle border for unselected width: isSelected ? 2.5 : 1.0, ), boxShadow: isSelected ? [ BoxShadow( color: color.withOpacity(0.5), blurRadius: 4, offset: const Offset(0, 2), ) ] : [], ), child: isSelected ? Icon(Icons.check, color: ThemeData.estimateBrightnessForColor(color) == Brightness.dark ? Colors.white : Colors.black, size: 20) : null, ), ); }).toList(), ), const SizedBox(height: 24), // Add space before actions ], ), ), ), ), actionsPadding: const EdgeInsets.fromLTRB(24.0, 0.0, 24.0, 16.0), // Adjust actions padding actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), // Возвращаем false при отмене child: const Text('Отмена'), ), ElevatedButton.icon( icon: const Icon(Icons.save_alt_rounded), onPressed: _saveCategory, label: const Text('Сохранить'), style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), ), ], ); } } // Helper extension for capitalizing first letter extension StringExtension on String { String capitalizeFirst() { if (isEmpty) return this; return "${this[0].toUpperCase()}${substring(1)}"; } }