fixes list sms

This commit is contained in:
2025-07-18 13:30:40 +03:00
parent 812fc7f322
commit 1c88330a9f
15 changed files with 474 additions and 512 deletions
+2 -1
View File
@@ -1,7 +1,8 @@
{
"permissions": {
"allow": [
"Bash(flutter gen-l10n:*)"
"Bash(flutter gen-l10n:*)",
"Bash(flutter analyze:*)"
],
"deny": []
}
+10 -1
View File
@@ -21,6 +21,7 @@ import 'logic/auth/auth_bloc.dart';
import 'logic/category/category_cubit.dart';
import 'logic/settings/settings_cubit.dart';
import 'logic/sms/sms_cubit.dart';
import 'logic/sms/sms_transaction_cubit.dart';
import 'logic/sms/sms_settings_cubit.dart'; // Добавляем импорт
import 'logic/tag/tag_cubit.dart';
import 'logic/transaction/transaction_bloc.dart';
@@ -157,9 +158,16 @@ Future<void> initUserSpecificDependencies(String userId) async {
getIt<SmsService>(),
getIt<ISmsMessageRepository>(),
getIt<UserCubit>(),
));
// SMS Transaction
if (getIt.isRegistered<SmsTransactionCubit>()) {
await getIt.unregister<SmsTransactionCubit>();
}
getIt.registerSingleton<SmsTransactionCubit>(SmsTransactionCubit(
getIt<ISmsHandlerRepository>(),
getIt<TransactionBloc>(),
getIt<ISettingsRepository>()
getIt<ISmsMessageRepository>(),
));
// Категории
@@ -202,6 +210,7 @@ Future<void> resetUserSpecificDependencies() async {
await getIt.unregister<SettingsCubit>();
await getIt.unregister<TransactionBloc>();
await getIt.unregister<SmsCubit>();
await getIt.unregister<SmsTransactionCubit>();
await getIt.unregister<CategoryCubit>();
await getIt.unregister<TagCubit>();
}
+5 -1
View File
@@ -104,5 +104,9 @@
"processedFilter": "Processed",
"notRequiredFilter": "Not required",
"errorFilter": "Error",
"transactionCreated": "Transaction created"
"transactionCreated": "Transaction created",
"smsStatusProcessed": "Processed",
"smsStatusError": "Error",
"smsStatusNotRequired": "Not required",
"smsStatusPending": "Pending"
}
+24
View File
@@ -727,6 +727,30 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Transaction created'**
String get transactionCreated;
/// No description provided for @smsStatusProcessed.
///
/// In en, this message translates to:
/// **'Processed'**
String get smsStatusProcessed;
/// No description provided for @smsStatusError.
///
/// In en, this message translates to:
/// **'Error'**
String get smsStatusError;
/// No description provided for @smsStatusNotRequired.
///
/// In en, this message translates to:
/// **'Not required'**
String get smsStatusNotRequired;
/// No description provided for @smsStatusPending.
///
/// In en, this message translates to:
/// **'Pending'**
String get smsStatusPending;
}
class _AppLocalizationsDelegate
+12
View File
@@ -328,4 +328,16 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get transactionCreated => 'Transaction created';
@override
String get smsStatusProcessed => 'Processed';
@override
String get smsStatusError => 'Error';
@override
String get smsStatusNotRequired => 'Not required';
@override
String get smsStatusPending => 'Pending';
}
+12
View File
@@ -329,4 +329,16 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get transactionCreated => 'Транзакция создана';
@override
String get smsStatusProcessed => 'Обработано';
@override
String get smsStatusError => 'Ошибка';
@override
String get smsStatusNotRequired => 'Не требуется';
@override
String get smsStatusPending => 'Ожидает';
}
+5 -1
View File
@@ -104,5 +104,9 @@
"processedFilter": "Обработанные",
"notRequiredFilter": "Не требуется",
"errorFilter": "Ошибка",
"transactionCreated": "Транзакция создана"
"transactionCreated": "Транзакция создана",
"smsStatusProcessed": "Обработано",
"smsStatusError": "Ошибка",
"smsStatusNotRequired": "Не требуется",
"smsStatusPending": "Ожидает"
}
+10 -104
View File
@@ -1,16 +1,10 @@
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/services/sms_service.dart';
import 'package:budget_app/logic/user/user_cubit.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:logger/logger.dart';
import 'package:budget_app/data/repositories/interfaces/isms_message_repository.dart';
import 'package:budget_app/data/repositories/interfaces/isettings_repository.dart';
/// Cubit для управления состоянием SMS.
///
@@ -19,9 +13,6 @@ class SmsCubit extends Cubit<SmsState> {
final SmsService _smsService;
final ISmsMessageRepository _smsRepository;
final UserCubit _userCubit;
final ISmsHandlerRepository _smsHandlerRepository;
final TransactionBloc _transactionBloc;
final ISettingsRepository _settingsRepository;
final Logger _logger = Logger();
List<SmsMessage> _messages = [];
@@ -37,9 +28,6 @@ class SmsCubit extends Cubit<SmsState> {
this._smsService,
this._smsRepository,
this._userCubit,
this._smsHandlerRepository,
this._transactionBloc,
this._settingsRepository,
) : super(SmsInitial());
/// Загружает последние 10 SMS сообщений.
@@ -95,16 +83,8 @@ class SmsCubit extends Cubit<SmsState> {
_currentPage = 1;
emit(const SmsState.loaded());
// 8. Обрабатываем новые сообщения (если разрешено в настройках)
final appSettings = await _settingsRepository.getSettings();
if (appSettings.autoCreateTransactionsFromSms) {
for (final message in newMessages) {
// Обрабатываем только сообщения со статусом "pending"
if (message.processingStatus == SmsProcessingStatus.pending) {
_createTransactionFromSms(message);
}
}
}
// 8. Новые сообщения загружены - обработка транзакций теперь в SmsTransactionCubit
// Примечание: автоматическая обработка SMS должна быть настроена в UI
} else {
emit(SmsError("User not loaded"));
}
@@ -141,91 +121,17 @@ class SmsCubit extends Cubit<SmsState> {
}
}
/// Создает транзакцию из СМС сообщения
Future<void> createTransactionFromSms(SmsMessage message) async {
try {
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));
// Обновляем сообщение с ссылкой на транзакцию и статусом "processed"
_logger.i('Creating transaction from SMS: ${message.id}');
_logger.d('Transaction details: ${transaction.toMap()}');
_logger.d('Before setting transactionId: ${message.transactionId}');
message.transactionId = transaction.id;
_logger.d('After setting transactionId: ${message.transactionId}');
// Обновляем статус сообщения на "processed"
final updatedMessage = message.copyWith(
transactionId: transaction.id,
processingStatus: SmsProcessingStatus.processed,
);
_logger.d('Calling updateMessage...');
await updateMessage(updatedMessage);
_logger.d('After updateMessage call');
_logger.i('Transaction created successfully for SMS: ${message.id}');
_logger.i('Final transactionId: ${message.transactionId}');
// Показываем уведомление об успешном создании транзакции
emit(const SmsState.transactionCreated());
} else {
// Обновляем статус сообщения на "error" при неудачном создании транзакции
final updatedMessage = message.copyWith(
processingStatus: SmsProcessingStatus.error,
errorMessage: 'Не удалось создать транзакцию',
);
await updateMessage(updatedMessage);
emit(const SmsState.transactionError('Не удалось создать транзакцию'));
}
} else {
// Обновляем статус сообщения на "error" при отсутствии функции обработки
final updatedMessage = message.copyWith(
processingStatus: SmsProcessingStatus.error,
errorMessage: 'Функция обработки не найдена для id: ${rule.customFunctionId}',
);
await updateMessage(updatedMessage);
emit(SmsState.transactionError(
'Функция обработки не найдена для id: ${rule.customFunctionId}',
));
}
} else {
// Обновляем статус сообщения на "notRequired" при отсутствии правила
final updatedMessage = message.copyWith(
processingStatus: SmsProcessingStatus.notRequired,
errorMessage: 'Правило для создания транзакции не найдено',
);
await updateMessage(updatedMessage);
emit(const SmsState.transactionError('Правило для создания транзакции не найдено'));
}
} catch (e) {
// Обновляем статус сообщения на "error" при любой ошибке
final updatedMessage = message.copyWith(
processingStatus: SmsProcessingStatus.error,
errorMessage: 'Ошибка создания транзакции: $e',
);
await updateMessage(updatedMessage);
emit(SmsState.transactionError('Ошибка создания транзакции: $e'));
/// Callback для обновления сообщения после обработки транзакции
void onMessageUpdated(SmsMessage updatedMessage) {
_messages = _messages.map((m) =>
m.id == updatedMessage.id ? updatedMessage : m
).toList();
if (state is SmsLoaded) {
emit(const SmsState.loaded());
}
}
Future<void> _createTransactionFromSms(SmsMessage sms) async {
// Оставлю заглушку для совместимости
await createTransactionFromSms(sms);
}
/// Загружает следующую страницу сообщений
Future<void> loadMoreMessages() async {
if (state is! SmsLoaded) return;
-7
View File
@@ -9,11 +9,4 @@ class SmsState with _$SmsState {
const factory SmsState.loaded() = SmsLoaded;
const factory SmsState.permissionDenied() = SmsPermissionDenied;
const factory SmsState.error(String message) = SmsError;
const factory SmsState.transactionLoading() = SmsTransactionLoading;
const factory SmsState.transactionError(String message) = SmsTransactionError;
const factory SmsState.transactionCreated() = SmsTransactionCreated;
const factory SmsState.transactionNotification(
String message, {
@Default(true) bool isSuccess,
}) = SmsTransactionNotification;
}
+12 -234
View File
@@ -55,7 +55,7 @@ extension SmsStatePatterns on SmsState {
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>({TResult Function( SmsInitial value)? initial,TResult Function( SmsLoading value)? loading,TResult Function( SmsLoaded value)? loaded,TResult Function( SmsPermissionDenied value)? permissionDenied,TResult Function( SmsError value)? error,TResult Function( SmsTransactionLoading value)? transactionLoading,TResult Function( SmsTransactionError value)? transactionError,TResult Function( SmsTransactionCreated value)? transactionCreated,TResult Function( SmsTransactionNotification value)? transactionNotification,required TResult orElse(),}){
@optionalTypeArgs TResult maybeMap<TResult extends Object?>({TResult Function( SmsInitial value)? initial,TResult Function( SmsLoading value)? loading,TResult Function( SmsLoaded value)? loaded,TResult Function( SmsPermissionDenied value)? permissionDenied,TResult Function( SmsError value)? error,required TResult orElse(),}){
final _that = this;
switch (_that) {
case SmsInitial() when initial != null:
@@ -63,11 +63,7 @@ return initial(_that);case SmsLoading() when loading != null:
return loading(_that);case SmsLoaded() when loaded != null:
return loaded(_that);case SmsPermissionDenied() when permissionDenied != null:
return permissionDenied(_that);case SmsError() when error != null:
return error(_that);case SmsTransactionLoading() when transactionLoading != null:
return transactionLoading(_that);case SmsTransactionError() when transactionError != null:
return transactionError(_that);case SmsTransactionCreated() when transactionCreated != null:
return transactionCreated(_that);case SmsTransactionNotification() when transactionNotification != null:
return transactionNotification(_that);case _:
return error(_that);case _:
return orElse();
}
@@ -85,7 +81,7 @@ return transactionNotification(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>({required TResult Function( SmsInitial value) initial,required TResult Function( SmsLoading value) loading,required TResult Function( SmsLoaded value) loaded,required TResult Function( SmsPermissionDenied value) permissionDenied,required TResult Function( SmsError value) error,required TResult Function( SmsTransactionLoading value) transactionLoading,required TResult Function( SmsTransactionError value) transactionError,required TResult Function( SmsTransactionCreated value) transactionCreated,required TResult Function( SmsTransactionNotification value) transactionNotification,}){
@optionalTypeArgs TResult map<TResult extends Object?>({required TResult Function( SmsInitial value) initial,required TResult Function( SmsLoading value) loading,required TResult Function( SmsLoaded value) loaded,required TResult Function( SmsPermissionDenied value) permissionDenied,required TResult Function( SmsError value) error,}){
final _that = this;
switch (_that) {
case SmsInitial():
@@ -93,11 +89,7 @@ return initial(_that);case SmsLoading():
return loading(_that);case SmsLoaded():
return loaded(_that);case SmsPermissionDenied():
return permissionDenied(_that);case SmsError():
return error(_that);case SmsTransactionLoading():
return transactionLoading(_that);case SmsTransactionError():
return transactionError(_that);case SmsTransactionCreated():
return transactionCreated(_that);case SmsTransactionNotification():
return transactionNotification(_that);case _:
return error(_that);case _:
throw StateError('Unexpected subclass');
}
@@ -114,7 +106,7 @@ return transactionNotification(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>({TResult? Function( SmsInitial value)? initial,TResult? Function( SmsLoading value)? loading,TResult? Function( SmsLoaded value)? loaded,TResult? Function( SmsPermissionDenied value)? permissionDenied,TResult? Function( SmsError value)? error,TResult? Function( SmsTransactionLoading value)? transactionLoading,TResult? Function( SmsTransactionError value)? transactionError,TResult? Function( SmsTransactionCreated value)? transactionCreated,TResult? Function( SmsTransactionNotification value)? transactionNotification,}){
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>({TResult? Function( SmsInitial value)? initial,TResult? Function( SmsLoading value)? loading,TResult? Function( SmsLoaded value)? loaded,TResult? Function( SmsPermissionDenied value)? permissionDenied,TResult? Function( SmsError value)? error,}){
final _that = this;
switch (_that) {
case SmsInitial() when initial != null:
@@ -122,11 +114,7 @@ return initial(_that);case SmsLoading() when loading != null:
return loading(_that);case SmsLoaded() when loaded != null:
return loaded(_that);case SmsPermissionDenied() when permissionDenied != null:
return permissionDenied(_that);case SmsError() when error != null:
return error(_that);case SmsTransactionLoading() when transactionLoading != null:
return transactionLoading(_that);case SmsTransactionError() when transactionError != null:
return transactionError(_that);case SmsTransactionCreated() when transactionCreated != null:
return transactionCreated(_that);case SmsTransactionNotification() when transactionNotification != null:
return transactionNotification(_that);case _:
return error(_that);case _:
return null;
}
@@ -143,18 +131,14 @@ return transactionNotification(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function()? initial,TResult Function()? loading,TResult Function()? loaded,TResult Function()? permissionDenied,TResult Function( String message)? error,TResult Function()? transactionLoading,TResult Function( String message)? transactionError,TResult Function()? transactionCreated,TResult Function( String message, bool isSuccess)? transactionNotification,required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function()? initial,TResult Function()? loading,TResult Function()? loaded,TResult Function()? permissionDenied,TResult Function( String message)? error,required TResult orElse(),}) {final _that = this;
switch (_that) {
case SmsInitial() when initial != null:
return initial();case SmsLoading() when loading != null:
return loading();case SmsLoaded() when loaded != null:
return loaded();case SmsPermissionDenied() when permissionDenied != null:
return permissionDenied();case SmsError() when error != null:
return error(_that.message);case SmsTransactionLoading() when transactionLoading != null:
return transactionLoading();case SmsTransactionError() when transactionError != null:
return transactionError(_that.message);case SmsTransactionCreated() when transactionCreated != null:
return transactionCreated();case SmsTransactionNotification() when transactionNotification != null:
return transactionNotification(_that.message,_that.isSuccess);case _:
return error(_that.message);case _:
return orElse();
}
@@ -172,18 +156,14 @@ return transactionNotification(_that.message,_that.isSuccess);case _:
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function() initial,required TResult Function() loading,required TResult Function() loaded,required TResult Function() permissionDenied,required TResult Function( String message) error,required TResult Function() transactionLoading,required TResult Function( String message) transactionError,required TResult Function() transactionCreated,required TResult Function( String message, bool isSuccess) transactionNotification,}) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function() initial,required TResult Function() loading,required TResult Function() loaded,required TResult Function() permissionDenied,required TResult Function( String message) error,}) {final _that = this;
switch (_that) {
case SmsInitial():
return initial();case SmsLoading():
return loading();case SmsLoaded():
return loaded();case SmsPermissionDenied():
return permissionDenied();case SmsError():
return error(_that.message);case SmsTransactionLoading():
return transactionLoading();case SmsTransactionError():
return transactionError(_that.message);case SmsTransactionCreated():
return transactionCreated();case SmsTransactionNotification():
return transactionNotification(_that.message,_that.isSuccess);case _:
return error(_that.message);case _:
throw StateError('Unexpected subclass');
}
@@ -200,18 +180,14 @@ return transactionNotification(_that.message,_that.isSuccess);case _:
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function()? initial,TResult? Function()? loading,TResult? Function()? loaded,TResult? Function()? permissionDenied,TResult? Function( String message)? error,TResult? Function()? transactionLoading,TResult? Function( String message)? transactionError,TResult? Function()? transactionCreated,TResult? Function( String message, bool isSuccess)? transactionNotification,}) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function()? initial,TResult? Function()? loading,TResult? Function()? loaded,TResult? Function()? permissionDenied,TResult? Function( String message)? error,}) {final _that = this;
switch (_that) {
case SmsInitial() when initial != null:
return initial();case SmsLoading() when loading != null:
return loading();case SmsLoaded() when loaded != null:
return loaded();case SmsPermissionDenied() when permissionDenied != null:
return permissionDenied();case SmsError() when error != null:
return error(_that.message);case SmsTransactionLoading() when transactionLoading != null:
return transactionLoading();case SmsTransactionError() when transactionError != null:
return transactionError(_that.message);case SmsTransactionCreated() when transactionCreated != null:
return transactionCreated();case SmsTransactionNotification() when transactionNotification != null:
return transactionNotification(_that.message,_that.isSuccess);case _:
return error(_that.message);case _:
return null;
}
@@ -411,204 +387,6 @@ as String,
}
}
/// @nodoc
class SmsTransactionLoading implements SmsState {
const SmsTransactionLoading();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SmsTransactionLoading);
}
@override
int get hashCode => runtimeType.hashCode;
@override
String toString() {
return 'SmsState.transactionLoading()';
}
}
/// @nodoc
class SmsTransactionError implements SmsState {
const SmsTransactionError(this.message);
final String message;
/// Create a copy of SmsState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SmsTransactionErrorCopyWith<SmsTransactionError> get copyWith => _$SmsTransactionErrorCopyWithImpl<SmsTransactionError>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SmsTransactionError&&(identical(other.message, message) || other.message == message));
}
@override
int get hashCode => Object.hash(runtimeType,message);
@override
String toString() {
return 'SmsState.transactionError(message: $message)';
}
}
/// @nodoc
abstract mixin class $SmsTransactionErrorCopyWith<$Res> implements $SmsStateCopyWith<$Res> {
factory $SmsTransactionErrorCopyWith(SmsTransactionError value, $Res Function(SmsTransactionError) _then) = _$SmsTransactionErrorCopyWithImpl;
@useResult
$Res call({
String message
});
}
/// @nodoc
class _$SmsTransactionErrorCopyWithImpl<$Res>
implements $SmsTransactionErrorCopyWith<$Res> {
_$SmsTransactionErrorCopyWithImpl(this._self, this._then);
final SmsTransactionError _self;
final $Res Function(SmsTransactionError) _then;
/// Create a copy of SmsState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') $Res call({Object? message = null,}) {
return _then(SmsTransactionError(
null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
class SmsTransactionCreated implements SmsState {
const SmsTransactionCreated();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SmsTransactionCreated);
}
@override
int get hashCode => runtimeType.hashCode;
@override
String toString() {
return 'SmsState.transactionCreated()';
}
}
/// @nodoc
class SmsTransactionNotification implements SmsState {
const SmsTransactionNotification(this.message, {this.isSuccess = true});
final String message;
@JsonKey() final bool isSuccess;
/// Create a copy of SmsState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SmsTransactionNotificationCopyWith<SmsTransactionNotification> get copyWith => _$SmsTransactionNotificationCopyWithImpl<SmsTransactionNotification>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SmsTransactionNotification&&(identical(other.message, message) || other.message == message)&&(identical(other.isSuccess, isSuccess) || other.isSuccess == isSuccess));
}
@override
int get hashCode => Object.hash(runtimeType,message,isSuccess);
@override
String toString() {
return 'SmsState.transactionNotification(message: $message, isSuccess: $isSuccess)';
}
}
/// @nodoc
abstract mixin class $SmsTransactionNotificationCopyWith<$Res> implements $SmsStateCopyWith<$Res> {
factory $SmsTransactionNotificationCopyWith(SmsTransactionNotification value, $Res Function(SmsTransactionNotification) _then) = _$SmsTransactionNotificationCopyWithImpl;
@useResult
$Res call({
String message, bool isSuccess
});
}
/// @nodoc
class _$SmsTransactionNotificationCopyWithImpl<$Res>
implements $SmsTransactionNotificationCopyWith<$Res> {
_$SmsTransactionNotificationCopyWithImpl(this._self, this._then);
final SmsTransactionNotification _self;
final $Res Function(SmsTransactionNotification) _then;
/// Create a copy of SmsState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') $Res call({Object? message = null,Object? isSuccess = null,}) {
return _then(SmsTransactionNotification(
null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
as String,isSuccess: null == isSuccess ? _self.isSuccess : isSuccess // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
// dart format on
+128
View File
@@ -0,0 +1,128 @@
import 'package:budget_app/logic/sms/sms_transaction_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/data/repositories/interfaces/isms_message_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:logger/logger.dart';
/// Cubit для обработки транзакций из SMS сообщений
class SmsTransactionCubit extends Cubit<SmsTransactionState> {
final ISmsHandlerRepository _smsHandlerRepository;
final TransactionBloc _transactionBloc;
final ISmsMessageRepository _smsRepository;
final Logger _logger = Logger();
SmsTransactionCubit(
this._smsHandlerRepository,
this._transactionBloc,
this._smsRepository,
) : super(const SmsTransactionInitial());
/// Создает транзакцию из SMS сообщения
Future<void> createTransactionFromSms(
SmsMessage message,
Function(SmsMessage) onMessageUpdated,
) async {
emit(const SmsTransactionProcessing());
try {
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));
_logger.i('Creating transaction from SMS: ${message.id}');
_logger.d('Transaction details: ${transaction.toMap()}');
// Обновляем сообщение с ссылкой на транзакцию и статусом "processed"
final updatedMessage = message.copyWith(
transactionId: transaction.id,
processingStatus: SmsProcessingStatus.processed,
);
await _smsRepository.update(updatedMessage);
onMessageUpdated(updatedMessage);
_logger.i('Transaction created successfully for SMS: ${message.id}');
emit(const SmsTransactionCreated());
} else {
// Обновляем статус сообщения на "error" при неудачном создании транзакции
final updatedMessage = message.copyWith(
processingStatus: SmsProcessingStatus.error,
errorMessage: 'Не удалось создать транзакцию',
);
await _smsRepository.update(updatedMessage);
onMessageUpdated(updatedMessage);
emit(const SmsTransactionError('Не удалось создать транзакцию'));
}
} else {
// Обновляем статус сообщения на "error" при отсутствии функции обработки
final updatedMessage = message.copyWith(
processingStatus: SmsProcessingStatus.error,
errorMessage: 'Функция обработки не найдена для id: ${rule.customFunctionId}',
);
await _smsRepository.update(updatedMessage);
onMessageUpdated(updatedMessage);
emit(SmsTransactionError(
'Функция обработки не найдена для id: ${rule.customFunctionId}',
));
}
} else {
// Обновляем статус сообщения на "notRequired" при отсутствии правила
final updatedMessage = message.copyWith(
processingStatus: SmsProcessingStatus.notRequired,
errorMessage: 'Правило для создания транзакции не найдено',
);
await _smsRepository.update(updatedMessage);
onMessageUpdated(updatedMessage);
emit(const SmsTransactionError('Правило для создания транзакции не найдено'));
}
} catch (e) {
// Обновляем статус сообщения на "error" при любой ошибке
final updatedMessage = message.copyWith(
processingStatus: SmsProcessingStatus.error,
errorMessage: 'Ошибка создания транзакции: $e',
);
await _smsRepository.update(updatedMessage);
onMessageUpdated(updatedMessage);
emit(SmsTransactionError('Ошибка создания транзакции: $e'));
}
}
/// Обрабатывает SMS сообщение (wrapper для совместимости)
Future<void> processMessage(
SmsMessage message,
Function(SmsMessage) onMessageUpdated,
) async {
await createTransactionFromSms(message, onMessageUpdated);
}
/// Сбрасывает состояние в начальное
void resetState() {
emit(const SmsTransactionInitial());
}
/// Отмечает уведомление как показанное
void markNotificationAsShown() {
final currentState = state;
if (currentState is SmsTransactionCreated && !currentState.wasShown) {
emit(SmsTransactionCreated(wasShown: true));
} else if (currentState is SmsTransactionError && !currentState.wasShown) {
emit(SmsTransactionError(currentState.message, wasShown: true));
}
}
}
+35
View File
@@ -0,0 +1,35 @@
import 'package:equatable/equatable.dart';
abstract class SmsTransactionState extends Equatable {
const SmsTransactionState();
@override
List<Object?> get props => [];
}
class SmsTransactionInitial extends SmsTransactionState {
const SmsTransactionInitial();
}
class SmsTransactionProcessing extends SmsTransactionState {
const SmsTransactionProcessing();
}
class SmsTransactionCreated extends SmsTransactionState {
final bool wasShown;
const SmsTransactionCreated({this.wasShown = false});
@override
List<Object?> get props => [wasShown];
}
class SmsTransactionError extends SmsTransactionState {
final String message;
final bool wasShown;
const SmsTransactionError(this.message, {this.wasShown = false});
@override
List<Object?> get props => [message, wasShown];
}
+13 -1
View File
@@ -1,3 +1,4 @@
import 'package:equatable/equatable.dart';
import 'package:hive_ce/hive.dart';
import '/utils/id_generator.dart';
@@ -17,7 +18,7 @@ enum SmsProcessingStatus {
}
@HiveType(typeId: 1004)
class SmsMessage extends HiveObject {
class SmsMessage extends HiveObject with EquatableMixin {
@HiveField(0)
final String id;
@@ -70,6 +71,17 @@ class SmsMessage extends HiveObject {
);
}
@override
List<Object?> get props => [
id,
body,
sender,
date,
transactionId,
processingStatus,
errorMessage,
];
/// Преобразует объект SmsMessage в Map
Map<String, dynamic> toMap() {
return {
+78 -78
View File
@@ -1,5 +1,7 @@
import 'package:budget_app/injection_container.dart' as di;
import 'package:budget_app/logic/sms/sms_settings_cubit.dart';
import 'package:budget_app/logic/sms/sms_transaction_cubit.dart';
import 'package:budget_app/logic/sms/sms_transaction_state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:budget_app/logic/sms/sms_cubit.dart';
@@ -47,6 +49,9 @@ class _SmsPageState extends State<SmsPage> {
BlocProvider(
create: (context) => SmsFilterCubit(context.read<SmsCubit>()),
),
BlocProvider.value(
value: di.getIt<SmsTransactionCubit>(),
),
],
child: Scaffold(
appBar: AppBar(
@@ -129,87 +134,82 @@ class _SmsPageState extends State<SmsPage> {
),
),
),
body: BlocBuilder<SmsCubit, SmsState>(
builder: (context, state) {
return state.when(
initial: () {
context.read<SmsCubit>().loadSmsMessages();
return const Center(child: CircularProgressIndicator());
},
loading: () => const Center(child: CircularProgressIndicator()),
loaded: () {
final cubit = context.read<SmsCubit>();
return BlocProvider(
create: (context) => di.getIt<SmsSettingsCubit>(),
child: NotificationListener<ScrollNotification>(
onNotification: (notification) {
if (notification is ScrollEndNotification &&
notification.metrics.pixels ==
notification.metrics.maxScrollExtent &&
cubit.hasMore) {
cubit.loadMoreMessages();
}
return false;
},
child: CustomScrollView(
slivers: [
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => SmsMessageWidget(
message: cubit.messages[index],
key: ValueKey(cubit.messages[index].id),
),
childCount: cubit.messages.length,
),
),
if (cubit.hasMore)
const SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Center(child: CircularProgressIndicator()),
),
),
],
),
),
);
},
permissionDenied: () => Center(
child: Text(AppLocalizations.of(context)!.smsPermissionDenied),
),
error: (message) => Center(
child: Text(message),
),
transactionLoading: () => const Center(child: CircularProgressIndicator()),
transactionError: (message) => Center(
child: Text(message),
),
transactionCreated: () => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.check_circle, color: Colors.green, size: 64),
const SizedBox(height: 16),
Text(AppLocalizations.of(context)!.transactionCreated),
],
body: BlocListener<SmsTransactionCubit, SmsTransactionState>(
listener: (context, state) {
if (state is SmsTransactionError && !state.wasShown) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
backgroundColor: Colors.red,
duration: const Duration(seconds: 2),
),
),
transactionNotification: (message, isSuccess) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
isSuccess ? Icons.check_circle : Icons.error,
color: isSuccess ? Colors.green : Colors.red,
size: 64,
),
const SizedBox(height: 16),
Text(message),
],
);
context.read<SmsTransactionCubit>().markNotificationAsShown();
} else if (state is SmsTransactionCreated && !state.wasShown) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.transactionCreated),
backgroundColor: Colors.green,
duration: const Duration(seconds: 2),
),
),
);
);
context.read<SmsTransactionCubit>().markNotificationAsShown();
}
},
child: BlocBuilder<SmsCubit, SmsState>(
builder: (context, state) {
return state.when(
initial: () {
context.read<SmsCubit>().loadSmsMessages();
return const Center(child: CircularProgressIndicator());
},
loading: () => const Center(child: CircularProgressIndicator()),
loaded: () {
final cubit = context.read<SmsCubit>();
return BlocProvider(
create: (context) => di.getIt<SmsSettingsCubit>(),
child: NotificationListener<ScrollNotification>(
onNotification: (notification) {
if (notification is ScrollEndNotification &&
notification.metrics.pixels ==
notification.metrics.maxScrollExtent &&
cubit.hasMore) {
cubit.loadMoreMessages();
}
return false;
},
child: CustomScrollView(
slivers: [
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => SmsMessageWidget(
message: cubit.messages[index],
key: ValueKey(cubit.messages[index].id),
),
childCount: cubit.messages.length,
),
),
if (cubit.hasMore)
const SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Center(child: CircularProgressIndicator()),
),
),
],
),
),
);
},
permissionDenied: () => Center(
child: Text(AppLocalizations.of(context)!.smsPermissionDenied),
),
error: (message) => Center(
child: Text(message),
),
);
},
),
),
),
);
+128 -84
View File
@@ -2,6 +2,8 @@ import 'package:budget_app/injection_container.dart' as di;
import 'package:budget_app/l10n/app_localizations.dart';
import 'package:budget_app/logic/sms/sms_cubit.dart';
import 'package:budget_app/logic/sms/sms_state.dart';
import 'package:budget_app/logic/sms/sms_transaction_cubit.dart';
import 'package:budget_app/logic/sms/sms_transaction_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';
@@ -44,7 +46,15 @@ class SmsMessageWidget extends StatelessWidget {
PopupMenuItem(
child: Text(AppLocalizations.of(context)!.createTransaction),
onTap: () {
context.read<SmsCubit>().createTransactionFromSms(message);
final smsCubit = context.read<SmsCubit>();
final currentMessage = smsCubit.messages.firstWhere(
(m) => m.id == message.id,
orElse: () => message,
);
context.read<SmsTransactionCubit>().createTransactionFromSms(
currentMessage,
smsCubit.onMessageUpdated,
);
},
),
],
@@ -53,92 +63,126 @@ class SmsMessageWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: di.getIt<SmsCubit>(),
child: BlocConsumer<SmsCubit, SmsState>(
listener: (context, state) {
if (state is SmsTransactionCreated) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(context)!.transactionCreatedSuccessfully),
backgroundColor: Colors.green,
),
);
} else if (state is SmsTransactionError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
backgroundColor: Colors.red,
),
);
}
},
builder: (context, state) {
if (state is SmsLoading || state is SmsTransactionLoading) {
return const Center(child: CircularProgressIndicator());
}
return GestureDetector(
onTapDown: (details) => _showPopupMenu(context, details),
child: Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.person_outline, size: 16),
const SizedBox(width: 8),
Expanded(
child: Text(
message.sender ??
AppLocalizations.of(context)!.unknownSender,
style: Theme.of(context).textTheme.titleSmall,
overflow: TextOverflow.ellipsis,
return BlocBuilder<SmsCubit, SmsState>(
builder: (context, smsState) {
final smsCubit = context.read<SmsCubit>();
final currentMessage = smsCubit.messages.firstWhere(
(m) => m.id == message.id,
orElse: () => message,
);
return BlocBuilder<SmsTransactionCubit, SmsTransactionState>(
builder: (context, transactionState) {
final isProcessing = transactionState is SmsTransactionProcessing;
return GestureDetector(
onTapDown: (details) => _showPopupMenu(context, details),
child: Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.person_outline, size: 16),
const SizedBox(width: 8),
Expanded(
child: Text(
currentMessage.sender ??
AppLocalizations.of(context)!.unknownSender,
style: Theme.of(context).textTheme.titleSmall,
overflow: TextOverflow.ellipsis,
),
),
),
const SizedBox(width: 16),
Text(
_formatDate(context, message.date),
style: Theme.of(context).textTheme.bodySmall,
),
],
),
const SizedBox(height: 12),
Text(
message.body ?? '',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 12),
Row(
children: [
Icon(
message.transactionId != null
? Icons.check_circle_outline
: Icons.error_outline,
size: 16,
color: message.transactionId != null
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.error,
),
const SizedBox(width: 4),
Text(
message.transactionId != null
? AppLocalizations.of(context)!.smsProcessed
: AppLocalizations.of(context)!.smsNotProcessed,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
],
const SizedBox(width: 16),
if (isProcessing)
const Padding(
padding: EdgeInsets.only(right: 8),
child: SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
),
Text(
_formatDate(context, currentMessage.date),
style: Theme.of(context).textTheme.bodySmall,
),
],
),
const SizedBox(height: 12),
Text(
currentMessage.body ?? '',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 12),
Row(
children: [
Icon(
_getStatusIcon(currentMessage),
size: 16,
color: _getStatusColor(context, currentMessage),
),
const SizedBox(width: 4),
Text(
_getStatusText(context, currentMessage),
style: Theme.of(context).textTheme.bodySmall,
),
],
),
],
),
),
),
),
);
},
),
);
},
);
},
);
}
IconData _getStatusIcon(SmsMessage msg) {
switch (msg.processingStatus) {
case SmsProcessingStatus.processed:
return Icons.check_circle_outline;
case SmsProcessingStatus.error:
return Icons.error_outline;
case SmsProcessingStatus.notRequired:
return Icons.info_outline;
case SmsProcessingStatus.pending:
default:
return Icons.pending_outlined;
}
}
Color _getStatusColor(BuildContext context, SmsMessage msg) {
switch (msg.processingStatus) {
case SmsProcessingStatus.processed:
return Theme.of(context).colorScheme.primary;
case SmsProcessingStatus.error:
return Theme.of(context).colorScheme.error;
case SmsProcessingStatus.notRequired:
return Theme.of(context).colorScheme.secondary;
case SmsProcessingStatus.pending:
default:
return Theme.of(context).colorScheme.outline;
}
}
String _getStatusText(BuildContext context, SmsMessage msg) {
switch (msg.processingStatus) {
case SmsProcessingStatus.processed:
return AppLocalizations.of(context)!.smsStatusProcessed;
case SmsProcessingStatus.error:
return msg.errorMessage ?? AppLocalizations.of(context)!.smsStatusError;
case SmsProcessingStatus.notRequired:
return AppLocalizations.of(context)!.smsStatusNotRequired;
case SmsProcessingStatus.pending:
default:
return AppLocalizations.of(context)!.smsStatusPending;
}
}
}