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

383 lines
16 KiB
Dart

import 'package:flutter/material.dart';
import 'package:drift/drift.dart' show Value;
import 'package:intl/intl.dart';
import '../database/database.dart' as db;
import '../utils/category_utils.dart'; // For icon and color utilities
class EditTransactionDialog extends StatefulWidget {
final db.AppDatabase database;
final db.Transaction transaction; // The transaction to edit
final Stream<List<db.CategoryDb>> categoriesStream; // Stream of available categories
const EditTransactionDialog({
Key? key,
required this.database,
required this.transaction,
required this.categoriesStream,
}) : super(key: key);
@override
State<EditTransactionDialog> createState() => _EditTransactionDialogState();
}
class _EditTransactionDialogState extends State<EditTransactionDialog> {
final _formKey = GlobalKey<FormState>();
late TextEditingController _amountController;
late TextEditingController _merchantController;
late String? _selectedCategoryName; // Can be null for Income
late DateTime _selectedDate;
late db.TransactionType _selectedType;
@override
void initState() {
super.initState();
// Initialize controllers and state with existing transaction data
_amountController = TextEditingController(text: widget.transaction.amount.toString());
_merchantController = TextEditingController(text: widget.transaction.merchant);
_selectedDate = widget.transaction.date;
_selectedType = widget.transaction.type == 'income' ? db.TransactionType.income : db.TransactionType.expense;
// Set initial category name based on transaction type
if (_selectedType == db.TransactionType.expense) {
_selectedCategoryName = widget.transaction.categoryName;
} else {
_selectedCategoryName = null; // Income doesn't have a selectable category
}
}
@override
void dispose() {
_amountController.dispose();
_merchantController.dispose();
super.dispose();
}
// Function to show the date and time pickers
Future<void> _selectDateTime(BuildContext context) async {
// 1. Pick Date
final DateTime? pickedDate = await showDatePicker(
context: context,
initialDate: _selectedDate,
firstDate: DateTime(2000),
lastDate: DateTime.now().add(const Duration(days: 365)),
);
if (pickedDate != null) {
// If date was picked, proceed to pick time
// 2. Pick Time
final TimeOfDay? pickedTime = await showTimePicker(
context: context,
initialTime: TimeOfDay.fromDateTime(_selectedDate),
);
if (pickedTime != null) {
// If time was also picked, combine date and time and update state
setState(() {
_selectedDate = DateTime(
pickedDate.year,
pickedDate.month,
pickedDate.day,
pickedTime.hour,
pickedTime.minute,
);
});
} else {
// If only date was picked, update state with the picked date and existing time
setState(() {
_selectedDate = DateTime(
pickedDate.year,
pickedDate.month,
pickedDate.day,
_selectedDate.hour, // Keep existing hour
_selectedDate.minute, // Keep existing minute
);
});
}
}
}
// Function to handle form submission (Update)
void _updateTransaction() async {
if (_formKey.currentState!.validate()) {
final amount = double.tryParse(_amountController.text);
if (amount == null || amount <= 0) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Пожалуйста, введите корректную положительную сумму.'),
backgroundColor: Colors.red,
),
);
return;
}
String categoryToSave;
String typeString = _selectedType == db.TransactionType.income ? 'income' : 'expense';
if (_selectedType == db.TransactionType.income) {
categoryToSave = 'Income'; // Fixed category for income
} else {
if (_selectedCategoryName == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Пожалуйста, выберите категорию для расхода.'),
backgroundColor: Colors.red,
),
);
return;
}
categoryToSave = _selectedCategoryName!;
}
// Create the updated transaction companion
final updatedTransaction = db.TransactionsCompanion(
id: Value(widget.transaction.id), // Include the ID for update
categoryName: Value(categoryToSave),
amount: Value(amount),
date: Value(_selectedDate),
merchant: Value(_merchantController.text),
type: Value(typeString),
);
try {
// Update transaction in the database
final success = await widget.database.updateTransaction(updatedTransaction);
if (success) {
// Close the dialog and return the updated transaction
if (mounted) Navigator.of(context).pop(widget.transaction.copyWith( // Return a copy with updated values
categoryName: categoryToSave,
amount: amount,
date: _selectedDate,
merchant: _merchantController.text,
type: typeString,
));
} else {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Не удалось обновить транзакцию.'),
backgroundColor: Colors.red,
),
);
}
}
} catch (e) {
print('Error updating transaction: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Ошибка при обновлении транзакции: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
final bool isIncome = _selectedType == db.TransactionType.income;
return AlertDialog(
title: const Text('Редактировать транзакцию'),
content: SingleChildScrollView( // Use SingleChildScrollView for content
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// --- Transaction Type Selector ---
Center(
child: ToggleButtons(
isSelected: [!isIncome, isIncome],
onPressed: (int index) {
setState(() {
_selectedType = index == 0 ? db.TransactionType.expense : db.TransactionType.income;
// Reset category selection if switching to income
if (_selectedType == db.TransactionType.income) {
_selectedCategoryName = null;
} else {
// If switching to expense, try to select the first expense category
// This relies on the StreamBuilder below to update the dropdown
// and potentially set a default if _selectedCategoryName is null.
}
});
},
borderRadius: BorderRadius.circular(12),
// ИЗМЕНЕНО: Уменьшена минимальная ширина кнопок
constraints: BoxConstraints(minWidth: (MediaQuery.of(context).size.width - 160) / 2, minHeight: 40), // Adjusted width for dialog
selectedColor: Colors.white,
fillColor: isIncome ? Colors.green.shade400 : Colors.red.shade400,
color: isDark ? Colors.white70 : Colors.black54,
selectedBorderColor: isIncome ? Colors.green.shade600 : Colors.red.shade600,
borderColor: isDark ? Colors.grey.shade600 : Colors.grey.shade400,
children: const <Widget>[
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [ Icon(Icons.arrow_upward_rounded, size: 18), SizedBox(width: 8), Text('Расход'), ],
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [ Icon(Icons.arrow_downward_rounded, size: 18), SizedBox(width: 8), Text('Доход'), ],
),
),
],
),
),
const SizedBox(height: 20),
// --- Amount Field ---
TextFormField(
controller: _amountController,
decoration: InputDecoration(
labelText: 'Сумма',
prefixIcon: Icon(Icons.attach_money, color: isIncome ? Colors.green : theme.colorScheme.primary),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
filled: true,
fillColor: isDark ? Colors.grey.shade800.withOpacity(0.5) : Colors.grey.shade100,
),
keyboardType: const TextInputType.numberWithOptions(decimal: true),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Пожалуйста, введите сумму';
}
if (double.tryParse(value) == null || double.parse(value) <= 0) {
return 'Пожалуйста, введите корректное положительное число';
}
return null;
},
),
const SizedBox(height: 16),
// --- Category Dropdown (Only for Expenses, uses StreamBuilder) ---
if (!isIncome)
StreamBuilder<List<db.CategoryDb>>(
stream: widget.categoriesStream,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting && !snapshot.hasData) {
return const Center(child: CircularProgressIndicator(strokeWidth: 2));
}
if (snapshot.hasError) {
return Text('Ошибка загрузки категорий: ${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
// This handles the case when switching from Income to Expense
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 DropdownButtonFormField<String>(
value: _selectedCategoryName,
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: 12.0, vertical: 16.0),
),
items: expenseCategories.map((db.CategoryDb category) {
final iconData = CategoryUtils.getIconFromString(category.icon);
final colorData = Color(category.color);
return DropdownMenuItem<String>(
value: category.name,
child: Row(
children: [
Icon(iconData, color: colorData, size: 20),
const SizedBox(width: 10),
Text(category.name),
],
),
);
}).toList(),
onChanged: (String? newValue) {
setState(() {
_selectedCategoryName = newValue;
});
},
validator: (value) {
if (_selectedType == db.TransactionType.expense && value == null) {
return 'Пожалуйста, выберите категорию';
}
return null;
},
);
},
),
if (!isIncome) const SizedBox(height: 16),
// --- Date and Time Picker ---
InkWell(
onTap: () => _selectDateTime(context),
child: InputDecorator(
decoration: InputDecoration(
labelText: 'Дата и время',
prefixIcon: const Icon(Icons.calendar_today_outlined),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
filled: true,
fillColor: isDark ? Colors.grey.shade800.withOpacity(0.5) : Colors.grey.shade100,
),
child: Text(
DateFormat.yMMMd().add_jm().format(_selectedDate),
style: theme.textTheme.bodyLarge,
),
),
),
const SizedBox(height: 16),
// --- Merchant / Source Field ---
TextFormField(
controller: _merchantController,
decoration: InputDecoration(
labelText: isIncome ? 'Источник' : 'Продавец / Магазин',
prefixIcon: Icon(isIncome ? Icons.source_outlined : Icons.storefront_outlined),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
filled: true,
fillColor: isDark ? Colors.grey.shade800.withOpacity(0.5) : Colors.grey.shade100,
),
textCapitalization: TextCapitalization.words,
),
],
),
),
),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.of(context).pop(), // Close dialog
child: const Text('Отмена'),
),
ElevatedButton(
onPressed: _updateTransaction, // Call update function
child: const Text('Сохранить'),
),
],
);
}
}