Move sms transaction create
This commit is contained in:
@@ -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>(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,6 +1,5 @@
|
||||
part of 'sms_settings_cubit.dart';
|
||||
|
||||
@immutable
|
||||
abstract class SmsSettingsState {
|
||||
const SmsSettingsState();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user