Move sms transaction create

This commit is contained in:
2025-07-16 17:52:23 +03:00
parent 9699433b3f
commit a9e1c02967
10 changed files with 127 additions and 125 deletions
+10 -2
View File
@@ -153,7 +153,13 @@ Future<void> initUserSpecificDependencies(String userId) async {
if (getIt.isRegistered<SmsCubit>()) {
await getIt.unregister<SmsCubit>();
}
getIt.registerFactory<SmsCubit>(() => SmsCubit(getIt(), getIt(), getIt()));
getIt.registerFactory<SmsCubit>(() => SmsCubit(
getIt<SmsService>(),
getIt<ISmsMessageRepository>(),
getIt<UserCubit>(),
getIt<ISmsHandlerRepository>(),
getIt<TransactionBloc>(),
));
// Категории
if (getIt.isRegistered<CategoryCubit>()) {
@@ -174,7 +180,9 @@ Future<void> initUserSpecificDependencies(String userId) async {
await getIt.unregister<SmsSettingsCubit>();
}
getIt.registerFactory<SmsSettingsCubit>(
() => SmsSettingsCubit(getIt<ISmsHandlerRepository>(), getIt<TransactionBloc>()),
() => SmsSettingsCubit(
getIt<ISmsHandlerRepository>(),
),
);
}
+64 -36
View File
@@ -1,5 +1,9 @@
import 'package:budget_app/logic/sms/sms_state.dart';
import 'package:budget_app/models/sms_message.dart';
import 'package:budget_app/data/repositories/interfaces/isms_handler_repository.dart';
import 'package:budget_app/logic/transaction/transaction_bloc.dart';
import 'package:budget_app/models/sms_handler_settings.dart';
import 'package:budget_app/services/custom_sms_functions.dart';
import 'package:budget_app/models/transaction_record.dart';
import 'package:budget_app/services/sms_service.dart';
import 'package:budget_app/logic/user/user_cubit.dart';
@@ -13,11 +17,15 @@ class SmsCubit extends Cubit<SmsState> {
final SmsService _smsService;
final ISmsMessageRepository _smsRepository;
final UserCubit _userCubit;
final ISmsHandlerRepository _smsHandlerRepository;
final TransactionBloc _transactionBloc;
SmsCubit(
this._smsService,
this._smsRepository,
this._userCubit
this._userCubit,
this._smsHandlerRepository,
this._transactionBloc,
) : super(SmsInitial());
/// Загружает последние 10 SMS сообщений.
@@ -80,43 +88,63 @@ class SmsCubit extends Cubit<SmsState> {
}
// Комментарий: Метод для создания транзакции из SMS-сообщения.
Future<void> _createTransactionFromSms(SmsMessage sms) async {
// Комментарий: Здесь будет логика для парсинга SMS и создания транзакции.
// Пример простой логики парсинга (нужно будет доработать под реальные SMS).
final body = sms.body?.toLowerCase() ?? '';
double? amount;
TransactionRecord? type;
// Комментарий: Поиск суммы в сообщении.
final amountRegex = RegExp(r'(\d+(\.\d{1,2})?)');
final match = amountRegex.firstMatch(body);
if (match != null) {
amount = double.tryParse(match.group(1)!);
/// Обновляет SMS сообщение в хранилище
Future<void> updateMessage(SmsMessage message) async {
try {
if (state is! SmsLoaded) return;
await _smsRepository.update(message);
final currentState = state as SmsLoaded;
final updatedMessages = currentState.messages.map((m) =>
m.id == message.id ? message : m
).toList();
emit(SmsLoaded(updatedMessages));
} catch (e) {
emit(SmsError('Ошибка обновления сообщения: $e'));
}
}
// TODO
// Комментарий: Определение типа транзакции (доход/расход).
// if (body.contains('покупка') || body.contains('списание')) {
// type = Transaction.expense;
// } else if (body.contains('зачисление') || body.contains('пополнение')) {
// type = TransactionType.income;
// }
/// Создает транзакцию из СМС сообщения
Future<void> createTransactionFromSms(SmsMessage message) async {
emit(SmsTransactionLoading());
try {
final settings = await _smsHandlerRepository.getSmsHandlerSettings();
final rule = settings?.rulesBySender[message.sender];
// if (amount != null && type != null) {
// // Комментарий: Создаем новую транзакцию.
// final transaction = TransactionRecord(
// amount: amount,
// type: type,
// date: sms.date ?? DateTime.now(),
// description: sms.body, // Описание берем из тела SMS
// // Комментарий: Здесь можно добавить логику для определения категории и тегов.
// );
// // Комментарий: Добавляем событие AddTransaction в TransactionBloc.
// _transactionBloc.add(AddTransaction(transaction));
// // Комментарий: Сохраняем ID транзакции в SMS-сообщении.
// await _smsRepository.update(
// sms.copyWith(transactionId: transaction.id)
// );
// }
if (rule != null &&
rule.type == SmsProcessingType.customFunction &&
rule.customFunctionId != null) {
final processingFunction = CustomSmsFunctions.getFunctionById(
rule.customFunctionId!,
);
if (processingFunction != null) {
final transaction = await processingFunction(message.body ?? '');
if (transaction != null) {
_transactionBloc.add(AddTransaction(transaction: transaction));
// Обновляем сообщение с ссылкой на транзакцию
message.transactionId = transaction.id;
await updateMessage(message);
emit(const SmsTransactionCreated());
} else {
emit(const SmsTransactionError('Не удалось создать транзакцию'));
}
} else {
emit(SmsTransactionError(
'Функция обработки не найдена для id: ${rule.customFunctionId}',
));
}
} else {
emit(const SmsTransactionError('Правило для создания транзакции не найдено'));
}
} catch (e) {
emit(SmsTransactionError('Ошибка создания транзакции: $e'));
}
}
Future<void> _createTransactionFromSms(SmsMessage sms) async {
// Оставлю заглушку для совместимости
await createTransactionFromSms(sms);
}
}
+3 -47
View File
@@ -1,9 +1,5 @@
import 'package:budget_app/data/repositories/interfaces/isms_handler_repository.dart';
import 'package:budget_app/logic/transaction/transaction_bloc.dart';
import 'package:budget_app/models/sms_handler_settings.dart';
import 'package:budget_app/models/sms_message.dart';
import 'package:budget_app/services/custom_sms_functions.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
part 'sms_settings_state.dart';
@@ -11,11 +7,11 @@ part 'sms_settings_state.dart';
/// Cubit для управления состоянием диалога настроек обработки SMS
class SmsSettingsCubit extends Cubit<SmsSettingsState> {
final ISmsHandlerRepository _smsHandlerRepository;
final TransactionBloc _transactionBloc;
SmsProcessingRule? _originalRule;
SmsSettingsCubit(this._smsHandlerRepository, this._transactionBloc)
: super(const SmsSettingsInitial());
SmsSettingsCubit(
this._smsHandlerRepository,
) : super(const SmsSettingsInitial());
/// Загружает правило для указанного отправителя
Future<void> loadRuleForSender(String sender) async {
@@ -68,44 +64,4 @@ class SmsSettingsCubit extends Cubit<SmsSettingsState> {
emit(SmsSettingsLoaded(_originalRule));
}
}
/// Создает транзакцию из СМС
Future<void> createTransactionFromSms(SmsMessage message) async {
try {
emit(const SmsSettingsLoading());
final settings = await _smsHandlerRepository.getSmsHandlerSettings();
final rule = settings?.rulesBySender[message.sender];
if (rule != null &&
rule.type == SmsProcessingType.customFunction &&
rule.customFunctionId != null) {
final processingFunction = CustomSmsFunctions.getFunctionById(
rule.customFunctionId!,
);
if (processingFunction != null) {
final transaction = await processingFunction(message.body ?? '');
if (transaction != null) {
_transactionBloc.add(AddTransaction(transaction: transaction));
print('Транзакция создана через TransactionBloc: $transaction');
emit(const SmsSettingsTransactionCreated());
} else {
emit(const SmsSettingsError('Не удалось создать транзакцию'));
}
} else {
emit(
SmsSettingsError(
'Функция обработки не найдена для id: ${rule.customFunctionId}',
),
);
}
} else {
// TODO: обработать случай, когда правило не найдено или тип не customFunction
emit(
const SmsSettingsError('Правило для создания транзакции не найдено'),
);
}
} catch (e) {
emit(SmsSettingsError('Ошибка создания транзакции: $e'));
}
}
}
-1
View File
@@ -1,6 +1,5 @@
part of 'sms_settings_cubit.dart';
@immutable
abstract class SmsSettingsState {
const SmsSettingsState();
}
+20
View File
@@ -37,3 +37,23 @@ class SmsError extends SmsState {
@override
List<Object> get props => [message];
}
/// Состояние загрузки при создании транзакции из SMS
class SmsTransactionLoading extends SmsState {
const SmsTransactionLoading();
}
/// Ошибка при обработке транзакции из SMS
class SmsTransactionError extends SmsState {
final String message;
const SmsTransactionError(this.message);
@override
List<Object> get props => [message];
}
/// Состояние успешного создания транзакции из SMS
class SmsTransactionCreated extends SmsState {
const SmsTransactionCreated();
}
+3
View File
@@ -142,6 +142,9 @@ class _TransactionsPageState extends State<TransactionsPage> {
final monthlyTransactions = state.transactions.where((t) {
return t.dateTime.month == _selectedMonth.month && t.dateTime.year == _selectedMonth.year;
}).toList();
// Сортируем транзакции по дате (от новых к старым)
monthlyTransactions.sort((a, b) => b.dateTime.compareTo(a.dateTime));
return ListView(
children: [
+11 -35
View File
@@ -1,29 +1,24 @@
import 'package:budget_app/injection_container.dart' as di;
import 'package:budget_app/l10n/app_localizations.dart';
import 'package:budget_app/logic/sms/sms_settings_cubit.dart';
import 'package:budget_app/logic/sms/sms_cubit.dart';
import 'package:budget_app/logic/sms/sms_state.dart';
import 'package:budget_app/models/sms_message.dart';
import 'package:budget_app/pages/sms/widgets/sms_settings_dialog.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
/// Виджет для отображения одного SMS сообщения с улучшенным интерфейсом.
///
/// Отображает отправителя, дату, тело сообщения и статус обработки.
/// Подсвечивает суммы в тексте сообщения.
class SmsMessageWidget extends StatelessWidget {
final SmsMessage message;
const SmsMessageWidget({super.key, required this.message});
// Форматирование даты для отображения
String _formatDate(BuildContext context, DateTime? date) {
if (date == null) return '';
final locale = Localizations.localeOf(context).toString();
return DateFormat.yMd(locale).add_jm().format(date);
}
// Функция для отображения всплывающего меню
void _showPopupMenu(BuildContext context, TapDownDetails details) {
final RenderBox overlay =
Overlay.of(context).context.findRenderObject() as RenderBox;
@@ -37,11 +32,10 @@ class SmsMessageWidget extends StatelessWidget {
PopupMenuItem(
child: Text(AppLocalizations.of(context)!.smsSettings),
onTap: () async {
// Показываем диалог настроек обработки SMS
await showDialog(
context: context,
builder: (context) => BlocProvider(
create: (context) => di.getIt.get<SmsSettingsCubit>(),
create: (context) => di.getIt.get<SmsCubit>(),
child: SmsSettingsDialog(sender: message.sender ?? ''),
),
);
@@ -50,10 +44,7 @@ class SmsMessageWidget extends StatelessWidget {
PopupMenuItem(
child: Text(AppLocalizations.of(context)!.createTransaction),
onTap: () {
// Создаем транзакцию из СМС
// Для этого мы передаем сообщение в SmsSettingsCubit
// который должен содержать логику обработки
context.read<SmsSettingsCubit>().createTransactionFromSms(message);
context.read<SmsCubit>().createTransactionFromSms(message);
},
),
],
@@ -63,38 +54,28 @@ class SmsMessageWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => di.getIt<SmsSettingsCubit>(),
child: BlocConsumer<SmsSettingsCubit, SmsSettingsState>(
create: (context) => di.getIt<SmsCubit>(),
child: BlocConsumer<SmsCubit, SmsState>(
listener: (context, state) {
if (state is SmsSettingsTransactionCreated) {
// Показываем SnackBar об успехе
if (state is SmsTransactionCreated) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(context)!.transactionCreatedSuccessfully,
),
AppLocalizations.of(context)!.transactionCreatedSuccessfully),
backgroundColor: Colors.green,
),
);
// Обновляем список сообщений
// Это можно сделать, вызвав метод из родительского виджета
// или используя callback
} else if (state is SmsSettingsError) {
// Показываем SnackBar об ошибке
} else if (state is SmsTransactionError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
state.errorMessage ??
AppLocalizations.of(context)!.unknownError,
),
content: Text(state.message),
backgroundColor: Colors.red,
),
);
}
},
builder: (context, state) {
// Отображаем индикатор загрузки, если нужно
if (state is SmsSettingsLoading) {
if (state is SmsLoading || state is SmsTransactionLoading) {
return const Center(child: CircularProgressIndicator());
}
return GestureDetector(
@@ -106,7 +87,6 @@ class SmsMessageWidget extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Заголовок: отправитель и дата
Row(
children: [
Icon(Icons.person_outline, size: 16),
@@ -127,15 +107,11 @@ class SmsMessageWidget extends StatelessWidget {
],
),
const SizedBox(height: 12),
// Тело сообщения с подсветкой сумм
Text(
message.body ?? '',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 12),
// Футер: статус обработки
Row(
children: [
Icon(
+6 -1
View File
@@ -3,6 +3,7 @@ import 'package:budget_app/models/category.dart';
import 'package:budget_app/models/transaction_record.dart';
import 'package:flutter/material.dart';
import 'package:budget_app/models/user.dart';
import '../utils/category_utils.dart';
/// Псевдоним для типа функции обработки СМС
typedef SmsProcessingFunction = Future<TransactionRecord?> Function(String smsBody);
@@ -47,12 +48,16 @@ class CustomSmsFunctions {
// Пример создания транзакции. В реальной реализации эти данные
// будут извлечены из СМС с помощью ИИ.
// Получаем стандартную категорию для AI транзакций
final aiCategory = CategoryUtils.getDefaultCategories()
.firstWhere((c) => c.id == 'ai_category');
return TransactionRecord(
amount: 123.45, // Пример суммы
dateTime: DateTime.now(),
vendor: 'AI Vendor',
currency: 'RUB',
category: Category(name: 'AI Category', isIncome: false, color: const Color(0xFF000000), icon: const IconData(0xea35, fontFamily: 'MaterialIcons')),
category: aiCategory,
);
}
}
+7
View File
@@ -68,6 +68,13 @@ class CategoryUtils {
icon: Icons.shopping_bag,
isIncome: false,
),
Category(
id: 'ai_category',
name: 'AI Транзакция',
color: Colors.black,
icon: Icons.smart_toy,
isIncome: false,
),
];
}
+3 -3
View File
@@ -33,7 +33,7 @@ class TransactionUtils {
TransactionRecord(
category: categories.firstWhere((c) => c.id == 'expense_food'),
tag: tags.firstWhere((t) => t.id == 'tag_family'),
amount: -3500,
amount: 3500,
dateTime: DateTime.now().subtract(const Duration(days: 1)),
vendor: 'Пятерочка',
currency: 'RUB',
@@ -41,7 +41,7 @@ class TransactionUtils {
TransactionRecord(
category: categories.firstWhere((c) => c.id == 'expense_transport'),
// tag: null, // Тег может быть null
amount: -500,
amount: 500,
dateTime: DateTime.now().subtract(const Duration(hours: 12)),
vendor: 'Яндекс Такси',
currency: 'RUB',
@@ -49,7 +49,7 @@ class TransactionUtils {
TransactionRecord(
category: categories.firstWhere((c) => c.id == 'expense_entertainment'),
tag: tags.firstWhere((t) => t.id == 'tag_friends'),
amount: -2000,
amount: 2000,
dateTime: DateTime.now().subtract(const Duration(hours: 6)),
vendor: 'Кинотеатр',
currency: 'RUB',