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.
268 lines
11 KiB
Dart
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)),
|
|
],
|
|
);
|
|
}
|
|
}
|