Files
FinanceApp2/lib/widgets/edit_category_dialog.dart
T
2025-05-12 19:08:22 +03:00

300 lines
13 KiB
Dart

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<EditCategoryDialog> createState() => _EditCategoryDialogState();
}
class _EditCategoryDialogState extends State<EditCategoryDialog> {
final _formKey = GlobalKey<FormState>();
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<void> _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<Color> 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<String>(
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<String>(
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)}";
}
}