feat: add category creation dialog and integration

This commit is contained in:
2025-05-04 22:47:36 +03:00
parent a0f3c016fa
commit c6d7018b4c
3 changed files with 369 additions and 94 deletions
+50 -10
View File
@@ -1,7 +1,7 @@
// ignore_for_file: unused_import // Часто генерируется при условном импорте
import 'package:drift/drift.dart';
import 'package:flutter/material.dart' as c;
import 'package:flutter/material.dart' as c; // Use alias 'c' for flutter material
// Условный импорт бэкенда базы данных
// Выбирает реализацию connect() в зависимости от платформы
import 'database_connection/connection.dart' // Базовый импорт
@@ -24,8 +24,8 @@ enum TransactionType { income, expense }
class Categories extends Table { // Changed class name to plural 'Categories'
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text().unique()();
TextColumn get icon => text()();
IntColumn get color => integer()();
TextColumn get icon => text()(); // Store icon name (e.g., 'shopping_cart_outlined')
IntColumn get color => integer()(); // Store color value (e.g., Colors.green.value)
}
// Определение таблицы Transactions
@@ -67,12 +67,26 @@ class AppDatabase extends _$AppDatabase {
// Drift автоматически обработает добавление новых таблиц (Categories)
// при обновлении до версии 4.
// Нам нужно только обработать специфичные изменения, как добавление колонки type.
if (from < 4) { // Check if upgrading from a version before 4
// Check if the 'type' column exists before trying to add it
// This requires a more complex check, usually involving inspecting the schema.
// For simplicity, we assume if version is < 4, the column might be missing.
// A safer approach involves querying PRAGMA table_info(transactions);
// However, Drift's default migration handles adding columns well.
// Let's ensure the Categories table is created if upgrading from very old versions.
await m.createTable(categories); // Ensure categories table exists
// Add the 'type' column if it doesn't exist (Drift might handle this, but explicit is safer)
// We'll rely on Drift's default behavior for adding the column here.
// If specific default values or constraints were needed during upgrade,
// more complex logic would be required.
}
if (from == 1) {
// Миграция с версии 1 на 2: добавляем колонку type
await m.addColumn(transactions, transactions.type);
// Example: If migrating specifically from 1, maybe add the type column
// await m.addColumn(transactions, transactions.type);
// But the check `from < 4` above is more general if relying on Drift's auto-migration.
}
// Add more migration steps for future versions here
// if (from < 3) { ... }
// if (from < 5) { ... }
// ИЗМЕНЕНО: Удален вызов insertInitialDataIfNeeded отсюда
// Не нужно вставлять начальные данные при обновлении существующей БД.
@@ -128,7 +142,31 @@ class AppDatabase extends _$AppDatabase {
return into(transactions).insert(entry);
}
// --- Методы для работы с категориями и суммами ---
// --- Методы для работы с категориями ---
// Получить все категории в виде потока, упорядоченные по имени
// Возвращает Stream<List<CategoryDb>> (CategoryDb - сгенерированный Drift класс)
Stream<List<CategoryDb>> watchAllCategoriesDb() {
return (select(categories)..orderBy([(c) => OrderingTerm(expression: c.name)])).watch();
}
// Добавить новую категорию
// Принимает CategoriesCompanion (сгенерированный Drift)
// Возвращает ID вставленной категории
Future<int> addCategory(CategoriesCompanion entry) {
// Проверяем, что имя, иконка и цвет предоставлены
assert(entry.name.present && entry.name.value.isNotEmpty);
assert(entry.icon.present); // Icon can be empty string if needed
assert(entry.color.present);
return into(categories).insert(entry);
}
// Получить категорию по ID (если нужно)
Future<CategoryDb?> getCategoryById(int id) {
return (select(categories)..where((c) => c.id.equals(id))).getSingleOrNull();
}
// --- Методы для агрегации и отчетов ---
// Вычислить и наблюдать за общими суммами по категориям РАСХОДОВ
// Возвращает Stream<List<Category>> где Category - это класс модели из '../models/category.dart'
@@ -151,6 +189,7 @@ class AppDatabase extends _$AppDatabase {
// 3. Преобразуем сгруппированные данные в список объектов Category (модель)
return categoryTotals.entries.map((entry) {
// Используем утилиту для получения деталей (иконка, цвет) по имени
// TODO: В будущем лучше получать детали напрямую из CategoryDb, если они там хранятся
final categoryDetails = CategoryUtils.getCategoryDetails(entry.key);
return Category( // Это Category из models/category.dart
entry.key, // name
@@ -201,15 +240,16 @@ class AppDatabase extends _$AppDatabase {
await batch((batch) {
batch.insertAll(categories, [
// Используем CategoriesCompanion (сгенерированный для таблицы Categories)
// Используем реальные имена иконок Material Icons и значения цветов
CategoriesCompanion.insert(name: 'Groceries', icon: 'shopping_cart_outlined', color: c.Colors.green.shade400.value),
CategoriesCompanion.insert(name: 'Subscriptions', icon: 'subscriptions_outlined', color: c.Colors.orange.shade400.value),
CategoriesCompanion.insert(name: 'Restaurant', icon: 'restaurant_menu_outlined', color: c.Colors.red.shade400.value),
CategoriesCompanion.insert(name: 'Shopping', icon: 'shopping_bag_outlined', color: c.Colors.blue.shade400.value),
CategoriesCompanion.insert(name: 'Transport', icon: 'directions_bus_filled_outlined', color: c.Colors.purple.shade400.value),
CategoriesCompanion.insert(name: 'Travel', icon: 'flight_takeoff_outlined', color: c.Colors.cyan.shade400.value),
CategoriesCompanion.insert(name: 'Utilities', icon: 'home_outlined', color: c.Colors.teal.shade400.value), // Example: Added Utilities
// 'Income' category might not be needed if handled separately, but can be added for consistency
CategoriesCompanion.insert(name: 'Income', icon: 'attach_money', color: c.Colors.lightGreenAccent.shade400.value), // Example: Added Income category
CategoriesCompanion.insert(name: 'Utilities', icon: 'home_outlined', color: c.Colors.teal.shade400.value),
// 'Income' category - use a specific icon and color
CategoriesCompanion.insert(name: 'Income', icon: 'attach_money', color: c.Colors.lightGreenAccent.shade700.value), // Changed color slightly
]);
});
print("Initial categories inserted.");
+220 -84
View File
@@ -18,6 +18,7 @@ import '../widgets/transaction_list_item.dart';
import '../widgets/filter_chip_widget.dart';
import '../utils/category_utils.dart'; // Import category utils
import 'profile_screen.dart'; // Import profile screen
import '../widgets/add_category_dialog.dart'; // Import the new dialog
// Enum for transaction type selection in the form
enum TransactionType { expense, income }
@@ -53,6 +54,7 @@ class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStat
late Stream<List<db.Transaction>> _transactionsStream; // Stream for transactions
late Stream<double> _totalIncomeStream; // Stream for total income
late Stream<double> _totalExpensesStream; // Stream for total expenses
late Stream<List<db.CategoryDb>> _categoriesStream; // Stream for categories from DB
@override
void initState() {
@@ -83,6 +85,7 @@ class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStat
_transactionsStream = widget.database.watchFilteredTransactions(_selectedFilter);
_totalIncomeStream = widget.database.watchTotalIncome();
_totalExpensesStream = widget.database.watchTotalExpenses(); // Use direct expense stream
_categoriesStream = widget.database.watchAllCategoriesDb(); // Watch categories from DB
// Start the pie chart appearance animation
_pieChartAnimationController.forward();
@@ -144,11 +147,14 @@ class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStat
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) {
// Pass the database instance to the form widget
// Pass the database instance and the categories stream to the form widget
return Padding(
// Add padding to account for the keyboard
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: _AddTransactionForm(database: widget.database),
child: _AddTransactionForm(
database: widget.database,
categoriesStream: _categoriesStream, // Pass the stream here
),
);
},
);
@@ -225,15 +231,28 @@ class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStat
_watchCategoryTotals(), // Stream<List<Category>> for pie chart (expenses only)
_totalIncomeStream, // Stream<double> for total income
_totalExpensesStream, // Stream<double> for total expenses
_categoriesStream, // Stream<List<db.CategoryDb>> for filter chips
],
builder: (context, snapshots) {
if (snapshots.length < 3) {
return const Center(child: CircularProgressIndicator());
// Check if all streams have data (or handle loading/error states individually)
if (snapshots.any((s) => s.connectionState == ConnectionState.waiting && !s.hasData)) {
return const Center(child: CircularProgressIndicator());
}
if (snapshots.any((s) => s.hasError)) {
// Find the first error and display it
final errorSnapshot = snapshots.firstWhere((s) => s.hasError);
return Center(child: Text('Error loading data: ${errorSnapshot.error}'));
}
final expenseCategories = snapshots[0].data as List<Category>? ?? [];
// Safely extract data with defaults
final expenseCategoriesForPie = snapshots[0].data as List<Category>? ?? [];
final totalIncome = snapshots[1].data as double? ?? 0.0;
final totalExpenses = snapshots[2].data as double? ?? 0.0;
final allDbCategories = snapshots[3].data as List<db.CategoryDb>? ?? [];
// Filter out the 'Income' category for display in expense filters/pie chart
final expenseDbCategories = allDbCategories.where((c) => c.name != 'Income').toList();
// Main column layout
return Column(
@@ -334,7 +353,8 @@ class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStat
onTap: _togglePieChartVisibility,
heightFactor: _pieChartHeightFactor, // Animation controller
child: SpendingPieChart(
categories: expenseCategories, // Pass EXPENSE categories from stream snapshot
// Use the categories calculated specifically for the pie chart
categories: expenseCategoriesForPie,
totalExpenses: totalExpenses, // Pass calculated total expenses
animation: _pieChartAnimation, // Appearance animation
),
@@ -394,7 +414,7 @@ class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStat
),
// --- Filter Chips Row (Animated Visibility) ---
// Shows 'All' and EXPENSE categories
// Shows 'All' and EXPENSE categories from the database stream
AnimatedContainer(
duration: const Duration(milliseconds: 300), // Animation duration
curve: Curves.easeInOut, // Animation curve
@@ -414,12 +434,12 @@ class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStat
isSelected: _selectedFilter == 'All',
onTap: () => _applyFilter('All')
),
// Dynamically generate filter chips from EXPENSE categories
...CategoryUtils.getAllCategoryNames().map((name) =>
// Dynamically generate filter chips from EXPENSE categories (from DB)
...expenseDbCategories.map((category) =>
FilterChipWidget(
label: name,
isSelected: _selectedFilter == name,
onTap: () => _applyFilter(name) // Applies expense category filter
label: category.name, // Use name from CategoryDb
isSelected: _selectedFilter == category.name,
onTap: () => _applyFilter(category.name) // Applies expense category filter
)
).toList(),
],
@@ -488,28 +508,39 @@ class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStat
itemBuilder: (context, index) {
// Get the db.Transaction object from the stream
final dbTransaction = transactions[index];
// Get the corresponding category details (icon, color) - use defaults for income
// FIX: Define icon/color directly for income, no CategoryDetails class
final ({IconData iconCode, Color colorCode}) categoryDetails = dbTransaction.type == 'expense'
? CategoryUtils.getCategoryDetails(dbTransaction.categoryName)
: ( // Default/Placeholder for Income type
iconCode: Icons.attach_money, // Or a more specific income icon
colorCode: Colors.green,
);
// Find the corresponding CategoryDb object for details (icon, color)
// This assumes category names are unique. Handle potential null if category deleted.
final categoryDb = allDbCategories.firstWhere(
(c) => c.name == dbTransaction.categoryName,
orElse: () => db.CategoryDb( // Provide a default if not found
id: -1,
name: dbTransaction.categoryName,
icon: 'help_outline', // Default icon
color: Colors.grey.value // Default color
),
);
// Get icon data from string name (using CategoryUtils as a helper for now)
// TODO: Improve icon handling (store IconData directly or use a better mapping)
final iconData = CategoryUtils.getIconFromString(categoryDb.icon);
final colorData = Color(categoryDb.color);
// Create the model.TransactionRecord needed by TransactionListItem
final transactionRecord = model.TransactionRecord(
id: dbTransaction.id,
type: dbTransaction.type,
amount: dbTransaction.amount,
category: dbTransaction.type == 'expense'
// Create the UI Category model only for expenses
category: dbTransaction.type == 'expense'
? Category(
dbTransaction.categoryName,
dbTransaction.amount,
categoryDetails.colorCode,
categoryDetails.iconCode,
dbTransaction.amount, // Amount here might be redundant?
colorData,
iconData,
)
: null,
: null, // No UI Category for income type
date: dbTransaction.date,
merchant: dbTransaction.merchant,
);
@@ -591,35 +622,60 @@ class MultiStreamBuilder extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StreamBuilder<List<AsyncSnapshot<dynamic>>>(
stream: _combineStreams(),
// Combine streams ensuring all emit at least one value (or handle initial nulls)
// Using StreamZip might wait until all streams emit. Behavior depends on stream types.
// Consider using combineLatest or similar if waiting isn't desired.
return StreamBuilder<List<dynamic>>( // Use List<dynamic> and check types later
stream: StreamZip(streams), // StreamZip waits for all streams to emit at least once
builder: (context, combinedSnapshot) {
if (combinedSnapshot.connectionState == ConnectionState.waiting) {
if (combinedSnapshot.connectionState == ConnectionState.waiting && !combinedSnapshot.hasData) {
// Show loading only if waiting AND no data has arrived yet
return const Center(child: CircularProgressIndicator());
}
if (combinedSnapshot.hasError) {
return Center(child: Text('Error: ${combinedSnapshot.error}'));
return Center(child: Text('Error combining streams: ${combinedSnapshot.error}'));
}
return builder(context, combinedSnapshot.data ?? []);
// Create AsyncSnapshot objects manually for the builder
// This allows handling individual stream states if needed, though StreamZip simplifies it
final snapshots = List<AsyncSnapshot<dynamic>>.generate(
streams.length,
(index) {
if (combinedSnapshot.hasData) {
// If combined stream has data, assume individual streams are done (or active with data)
return AsyncSnapshot.withData(ConnectionState.active, combinedSnapshot.data![index]);
} else if (combinedSnapshot.hasError) {
// Propagate error to individual snapshots (might need refinement)
return AsyncSnapshot.withError(ConnectionState.active, combinedSnapshot.error!);
} else {
// Default to waiting state if combined stream is waiting
return const AsyncSnapshot.waiting();
}
},
);
// Call the original builder function with the list of snapshots
return builder(context, snapshots);
},
);
}
Stream<List<AsyncSnapshot<dynamic>>> _combineStreams() {
return StreamZip(streams.map((stream) {
return stream.map((data) => AsyncSnapshot.withData(ConnectionState.done, data));
}));
}
// This helper function is no longer needed as StreamZip handles the combination
// Stream<List<AsyncSnapshot<dynamic>>> _combineStreams() { ... }
}
// --- Widget for the Add Transaction Form ---
class _AddTransactionForm extends StatefulWidget {
final db.AppDatabase database;
final Stream<List<db.CategoryDb>> categoriesStream; // Receive stream
const _AddTransactionForm({Key? key, required this.database}) : super(key: key);
const _AddTransactionForm({
Key? key,
required this.database,
required this.categoriesStream, // Require stream
}) : super(key: key);
@override
State<_AddTransactionForm> createState() => _AddTransactionFormState();
@@ -629,20 +685,21 @@ class _AddTransactionFormState extends State<_AddTransactionForm> {
final _formKey = GlobalKey<FormState>(); // Key for form validation
final _amountController = TextEditingController();
final _merchantController = TextEditingController(); // Label changes based on type
String? _selectedCategory; // Nullable initially, only for expenses
String? _selectedCategoryName; // Store the NAME of the selected category
DateTime _selectedDate = DateTime.now(); // Default to today, includes time
TransactionType _selectedType = TransactionType.expense; // Default to expense
// List of available categories fetched from utils (for expenses)
final List<String> _categories = CategoryUtils.getAllCategoryNames();
// No longer need static list: final List<String> _categories = CategoryUtils.getAllCategoryNames();
@override
void initState() {
super.initState();
// Set the initial category if the list is not empty and type is expense
if (_selectedType == TransactionType.expense && _categories.isNotEmpty) {
_selectedCategory = _categories[0];
}
// We need to listen to the stream for the initial value
// Setting initial value here is tricky with streams, better handle in StreamBuilder
// if (_selectedType == TransactionType.expense && _categories.isNotEmpty) {
// _selectedCategoryName = _categories[0];
// }
}
@override
@@ -724,7 +781,7 @@ class _AddTransactionFormState extends State<_AddTransactionForm> {
categoryToSave = 'Income'; // Use a fixed category for income
} else {
// Ensure a category is selected for expenses
if (_selectedCategory == null) {
if (_selectedCategoryName == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please select a category for the expense.'),
@@ -734,7 +791,7 @@ class _AddTransactionFormState extends State<_AddTransactionForm> {
);
return;
}
categoryToSave = _selectedCategory!;
categoryToSave = _selectedCategoryName!;
}
// Create the transaction companion including the type
@@ -783,6 +840,33 @@ class _AddTransactionFormState extends State<_AddTransactionForm> {
}
}
// --- Function to handle Add Category button press ---
void _showAddCategoryDialog() async {
// Show the dialog and wait for the result
final newCategory = await showDialog<db.CategoryDb?>( // Expecting CategoryDb or null
context: context,
builder: (context) => AddCategoryDialog(database: widget.database),
);
// If a new category was created and returned
if (newCategory != null) {
// Set the newly created category as selected in the dropdown
setState(() {
_selectedCategoryName = newCategory.name;
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Категория "${newCategory.name}" создана и выбрана.'),
duration: const Duration(seconds: 2),
),
);
}
}
}
// --- End of Add Category Function ---
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
@@ -821,13 +905,12 @@ class _AddTransactionFormState extends State<_AddTransactionForm> {
onPressed: (int index) {
setState(() {
_selectedType = index == 0 ? TransactionType.expense : TransactionType.income;
// Reset category selection if switching to income or if expense has no default
// Reset category selection if switching to income
if (_selectedType == TransactionType.income) {
_selectedCategory = null;
} else if (_categories.isNotEmpty) {
_selectedCategory = _categories[0]; // Reset to default for expense
_selectedCategoryName = null;
} else {
_selectedCategory = null;
// Don't reset to default here, let StreamBuilder handle initial state
// _selectedCategoryName = _categories[0]; // Remove this
}
});
},
@@ -882,44 +965,97 @@ class _AddTransactionFormState extends State<_AddTransactionForm> {
),
const SizedBox(height: 16),
// --- Category Dropdown (Only for Expenses) ---
// --- Category Dropdown and Add Button (Only for Expenses, uses StreamBuilder) ---
if (!isIncome)
DropdownButtonFormField<String>(
value: _selectedCategory,
decoration: InputDecoration(
labelText: 'Category',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
filled: true,
fillColor: isDark ? Colors.grey.shade800.withOpacity(0.5) : Colors.grey.shade100,
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 16.0),
),
items: _categories.map((String category) {
final details = CategoryUtils.getCategoryDetails(category);
return DropdownMenuItem<String>(
value: category,
child: Row(
children: [
Icon(details.iconCode, color: details.colorCode, size: 20),
const SizedBox(width: 10),
Text(category),
],
),
);
}).toList(),
onChanged: (String? newValue) {
setState(() {
_selectedCategory = newValue;
});
},
validator: (value) {
// Only validate if it's an expense
if (_selectedType == TransactionType.expense && value == null) {
return 'Please select a category';
StreamBuilder<List<db.CategoryDb>>(
stream: widget.categoriesStream,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting && !snapshot.hasData) {
return const Center(child: CircularProgressIndicator(strokeWidth: 2));
}
return null; // No validation needed for income
}
if (snapshot.hasError) {
return Text('Error loading categories: ${snapshot.error}');
}
final categoriesFromDb = snapshot.data ?? [];
// Filter out 'Income' category for the dropdown
final expenseCategories = categoriesFromDb.where((c) => c.name != 'Income').toList();
// Ensure _selectedCategoryName is valid or reset it
if (_selectedCategoryName != null && !expenseCategories.any((c) => c.name == _selectedCategoryName)) {
_selectedCategoryName = null; // Reset if selected category is no longer valid
}
// Set default selection if nothing is selected and list is not empty
if (_selectedCategoryName == null && expenseCategories.isNotEmpty) {
// Use WidgetsBinding to schedule state update after build
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) { // Check if widget is still mounted
setState(() {
_selectedCategoryName = expenseCategories[0].name;
});
}
});
}
return Row(
crossAxisAlignment: CrossAxisAlignment.start, // Align items to the top
children: [
// Dropdown takes most space
Expanded(
child: DropdownButtonFormField<String>(
value: _selectedCategoryName, // Use the name state variable
decoration: InputDecoration(
labelText: 'Category',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
filled: true,
fillColor: isDark ? Colors.grey.shade800.withOpacity(0.5) : Colors.grey.shade100,
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 16.0), // Adjust padding if needed
),
// Map CategoryDb objects to DropdownMenuItem<String>
items: expenseCategories.map((db.CategoryDb category) {
// TODO: Improve icon/color handling later
final iconData = CategoryUtils.getIconFromString(category.icon);
final colorData = Color(category.color);
return DropdownMenuItem<String>(
value: category.name, // Value is the category name (String)
child: Row(
children: [
Icon(iconData, color: colorData, size: 20),
const SizedBox(width: 10),
Text(category.name),
],
),
);
}).toList(),
onChanged: (String? newValue) {
setState(() {
_selectedCategoryName = newValue; // Update the selected name
});
},
validator: (value) {
// Only validate if it's an expense
if (_selectedType == TransactionType.expense && value == null) {
return 'Please select a category';
}
return null; // No validation needed for income
},
),
),
// Add Category Button
Padding(
padding: const EdgeInsets.only(left: 8.0, top: 8.0), // Add padding to space it out and align vertically
child: IconButton(
icon: Icon(Icons.add_circle_outline, color: theme.colorScheme.primary),
tooltip: 'Создать категорию', // Tooltip in Russian as requested
onPressed: _showAddCategoryDialog, // Call the function to show the dialog
),
),
],
);
},
),
if (!isIncome) const SizedBox(height: 16), // Spacer only if category is shown
if (!isIncome) const SizedBox(height: 16), // Spacer only if category row is shown
// --- Date and Time Picker ---
InkWell(
@@ -0,0 +1,99 @@
import 'package:drift/drift.dart' show Value;
import 'package:flutter/material.dart';
import '../database/database.dart' as db; // Import database with prefix 'db'
class AddCategoryDialog extends StatefulWidget {
final db.AppDatabase database;
const AddCategoryDialog({Key? key, required this.database}) : super(key: key);
@override
State<AddCategoryDialog> createState() => _AddCategoryDialogState();
}
class _AddCategoryDialogState extends State<AddCategoryDialog> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
// TODO: Add controllers/state for icon and color pickers later
@override
void dispose() {
_nameController.dispose();
super.dispose();
}
Future<void> _saveCategory() async {
if (_formKey.currentState!.validate()) {
final name = _nameController.text;
// TODO: Get selected icon and color
const icon = 'label_outline'; // Placeholder icon
final color = Colors.grey.shade400.value; // Placeholder color
final newCategoryCompanion = db.CategoriesCompanion(
name: Value(name),
icon: const Value(icon), // Use placeholder
color: Value(color), // Use placeholder
);
try {
final newCategoryId = await widget.database.addCategory(newCategoryCompanion);
final newCategory = await widget.database.getCategoryById(newCategoryId);
if (mounted) {
Navigator.pop(context, newCategory); // Return the newly created CategoryDb object
}
} catch (e) {
print('Error adding category: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error adding category: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Создать категорию'),
content: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
labelText: 'Название категории',
icon: Icon(Icons.label_outline),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Пожалуйста, введите название';
}
// TODO: Add validation to check if category name already exists
return null;
},
textCapitalization: TextCapitalization.sentences,
),
// TODO: Add Icon Picker here
// TODO: Add Color Picker here
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, null), // Return null if cancelled
child: const Text('Отмена'),
),
ElevatedButton(
onPressed: _saveCategory,
child: const Text('Сохранить'),
),
],
);
}
}