Files
BudgetApp/lib/pages/home/widgets/add_transaction_dialog.dart
T
Sanders 6da0da45b7 Adds category management feature
Adds the ability to manage categories in the settings page. This includes:
- Registers CategoryCubit in the dependency injection container.
- Introduces a new CategoryListPage for editing categories.
- Adds translations for category management related text.
- Adds unselected icon color to the theme.
- Updates the date format in the transaction dialog.
2025-06-30 15:59:43 +03:00

268 lines
11 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import 'package:intl/intl.dart';
import '../../../l10n/app_localizations.dart';
import '../../../logic/auth/auth_bloc.dart';
import '../../../logic/transaction/transaction_bloc.dart';
import '../../../data/repositories/interfaces/itag_repository.dart'; // Импортируем интерфейс репозитория тегов
import '../../../models/category.dart';
import '../../../models/tag.dart'; // Импортируем модель тега
import '../../../models/transaction_record.dart';
import '../../../services/user_service.dart';
import '../../../utils/category_utils.dart';
class AddTransactionDialog extends StatefulWidget {
const AddTransactionDialog({super.key});
@override
State<AddTransactionDialog> createState() => _AddTransactionDialogState();
}
class _AddTransactionDialogState extends State<AddTransactionDialog> {
final _formKey = GlobalKey<FormState>();
final _amountController = TextEditingController();
final _vendorController = TextEditingController();
final _dateController = TextEditingController();
bool _isIncome = false;
Category? _selectedCategory;
// Комментарий: Добавляем состояние для выбранного тега.
Tag? _selectedTag;
// Комментарий: Заменяем _selectedDate на _selectedDateTime для хранения даты и времени.
DateTime _selectedDateTime = DateTime.now();
@override
void initState() {
super.initState();
// Комментарий: Устанавливаем начальное значение с датой и временем.
_dateController.text = DateFormat('dd-MM-yyyy').add_Hm().format(_selectedDateTime);
}
@override
void dispose() {
_amountController.dispose();
_vendorController.dispose();
_dateController.dispose();
super.dispose();
}
// Комментарий: Этот метод теперь обрабатывает выбор и даты, и времени.
Future<void> _selectDateTime(BuildContext context) async {
final DateTime? pickedDate = await showDatePicker(
context: context,
initialDate: _selectedDateTime,
firstDate: DateTime(2000),
lastDate: DateTime(2101),
);
// Комментарий: Если пользователь не выбрал дату, выходим из функции.
if (pickedDate == null) return;
// ignore: use_build_context_synchronously
final TimeOfDay? pickedTime = await showTimePicker(
context: context,
initialTime: TimeOfDay.fromDateTime(_selectedDateTime),
);
// Комментарий: Если пользователь не выбрал время, выходим из функции.
if (pickedTime == null) return;
// Комментарий: Обновляем состояние с новой датой и временем.
setState(() {
_selectedDateTime = DateTime(
pickedDate.year,
pickedDate.month,
pickedDate.day,
pickedTime.hour,
pickedTime.minute,
);
// Комментарий: Обновляем текстовое поле с отформатированной датой и временем.
_dateController.text = DateFormat('dd-MM-yyyy').add_Hm().format(_selectedDateTime);
});
}
void _submitForm() {
if (_formKey.currentState!.validate()) {
final amount = double.tryParse(_amountController.text);
if (amount == null || _selectedCategory == null) {
// Показать ошибку, если сумма некорректна или категория не выбрана
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(
context,
)!.transactionErrorText('Invalid data'),
),
),
);
return;
}
final authState = context.read<AuthBloc>().state;
if (authState is! AuthAuthenticated) {
// Показать ошибку, если пользователь не аутентифицирован
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(
context,
)!.transactionErrorText('User not authenticated'),
),
),
);
return;
}
final newTransaction = TransactionRecord(
amount: amount,
vendor: _vendorController.text,
category: _selectedCategory!,
// Комментарий: Используем _selectedDateTime для сохранения точного времени транзакции.
dateTime: _selectedDateTime,
// Комментарий: Добавляем выбранный тег в транзакцию.
tag: _selectedTag,
currency:
GetIt.instance<UserService>().currentUser?.defaultCurrency ?? 'USD',
userId: authState.user.id,
);
context.read<TransactionBloc>().add(
AddTransaction(transaction: newTransaction),
);
Navigator.of(context).pop();
}
}
@override
Widget build(BuildContext context) {
final localizations = AppLocalizations.of(context)!;
final categories = CategoryUtils.getDefaultCategories(
(context.read<AuthBloc>().state as AuthAuthenticated).user.id,
).where((c) => c.isIncome == _isIncome).toList();
return AlertDialog(
title: Text(localizations.addTransactionButton),
content: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SwitchListTile(
title: Text(localizations.income),
value: _isIncome,
onChanged: (bool value) {
setState(() {
_isIncome = value;
_selectedCategory =
null; // Сбрасываем категорию при смене типа
});
},
),
TextFormField(
controller: _amountController,
decoration: InputDecoration(labelText: localizations.amount),
// Комментарий: Устанавливаем числовую клавиатуру с поддержкой десятичных чисел.
keyboardType:
const TextInputType.numberWithOptions(decimal: true),
// Комментарий: Добавляем фильтр для ввода только чисел и одной точки.
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*')),
],
validator: (value) {
if (value == null || value.isEmpty) {
return localizations.requiredField;
}
if (double.tryParse(value) == null) {
return localizations.invalidNumber;
}
return null;
},
),
TextFormField(
controller: _vendorController,
decoration: InputDecoration(labelText: localizations.vendor),
validator: (value) {
if (value == null || value.isEmpty) {
return localizations.requiredField;
}
return null;
},
),
DropdownButtonFormField<Category>(
value: _selectedCategory,
decoration: InputDecoration(labelText: localizations.category),
items: categories.map((Category category) {
return DropdownMenuItem<Category>(
value: category,
child: Text(category.name),
);
}).toList(),
onChanged: (Category? newValue) {
setState(() {
_selectedCategory = newValue;
});
},
validator: (value) =>
value == null ? localizations.requiredField : null,
),
// Комментарий: Добавляем выпадающий список для выбора тега.
// Он будет загружать теги асинхронно для текущего пользователя.
FutureBuilder<List<Tag>>(
future: GetIt.instance<ITagRepository>().getAllByUser(
(context.read<AuthBloc>().state as AuthAuthenticated).user.id,
),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
final tags = snapshot.data ?? [];
return DropdownButtonFormField<Tag>(
value: _selectedTag,
decoration: InputDecoration(labelText: localizations.tag),
items: tags.map((Tag tag) {
return DropdownMenuItem<Tag>(
value: tag,
child: Text(tag.name),
);
}).toList(),
onChanged: (Tag? newValue) {
setState(() {
_selectedTag = newValue;
});
},
// Комментарий: Тег не является обязательным полем.
);
},
),
TextFormField(
controller: _dateController,
decoration: InputDecoration(
labelText: localizations.date,
suffixIcon: IconButton(
icon: const Icon(Icons.calendar_today),
// Комментарий: Вызываем новый метод для выбора даты и времени.
onPressed: () => _selectDateTime(context),
),
),
readOnly: true,
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(localizations.cancel),
),
ElevatedButton(onPressed: _submitForm, child: Text(localizations.save)),
],
);
}
}