- Adds new localization keys for transaction details. - Deletes obsolete localization files, switching to a single ARB file per language. - Adds `Equatable` to data models (`Category`, `Tag`, `TransactionRecord`, `User`) for improved state management and change detection. - Introduces the transaction creation dialog. - Adds development note to GEMINI.md Improves internationalization and data models - Adds new localization keys for transaction details, supporting enhanced user experience. - Migrates to single ARB file for each language, streamlining the localization process. - Implements Equatable in data models (Category, Tag, TransactionRecord, User) for improved state management and simplified change detection. - Introduces transaction creation dialog, simplifying the transaction creation process. - Adds development note to GEMINI.md
201 lines
6.6 KiB
Dart
201 lines
6.6 KiB
Dart
import 'package:flutter/material.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 '../../../models/category.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;
|
|
DateTime _selectedDate = DateTime.now();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_dateController.text = DateFormat.yMd().format(_selectedDate);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_amountController.dispose();
|
|
_vendorController.dispose();
|
|
_dateController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _selectDate(BuildContext context) async {
|
|
final DateTime? picked = await showDatePicker(
|
|
context: context,
|
|
initialDate: _selectedDate,
|
|
firstDate: DateTime(2000),
|
|
lastDate: DateTime(2101),
|
|
);
|
|
if (picked != null && picked != _selectedDate) {
|
|
setState(() {
|
|
_selectedDate = picked;
|
|
_dateController.text = DateFormat.yMd().format(_selectedDate);
|
|
});
|
|
}
|
|
}
|
|
|
|
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!,
|
|
dateTime: _selectedDate,
|
|
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: TextInputType.number,
|
|
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,
|
|
),
|
|
TextFormField(
|
|
controller: _dateController,
|
|
decoration: InputDecoration(
|
|
labelText: localizations.date,
|
|
suffixIcon: IconButton(
|
|
icon: const Icon(Icons.calendar_today),
|
|
onPressed: () => _selectDate(context),
|
|
),
|
|
),
|
|
readOnly: true,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
child: Text(localizations.cancel),
|
|
),
|
|
ElevatedButton(onPressed: _submitForm, child: Text(localizations.save)),
|
|
],
|
|
);
|
|
}
|
|
}
|