Init
This commit is contained in:
commit
295625c70f
151 files changed
+10677
No files matched your search
@@ -0,0 +1,382 @@
|
||||
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('Сохранить'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user