Rework sms

This commit is contained in:
2025-07-19 13:04:17 +03:00
parent ce56101b2b
commit 7e32711383
11 changed files with 211 additions and 76 deletions
+1
View File
@@ -64,6 +64,7 @@
"ruleTypeLabel": "Processing type",
"regexpType": "Regular expression",
"customFunctionType": "Custom function",
"noProcessingType": "No processing required",
"ruleTypeRequired": "Processing type is required",
"regexpPatternHint": "Regular expression pattern",
"regexpPatternRequired": "Pattern is required",
+6
View File
@@ -482,6 +482,12 @@ abstract class AppLocalizations {
/// **'Custom function'**
String get customFunctionType;
/// No description provided for @noProcessingType.
///
/// In en, this message translates to:
/// **'No processing required'**
String get noProcessingType;
/// No description provided for @ruleTypeRequired.
///
/// In en, this message translates to:
+3
View File
@@ -203,6 +203,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get customFunctionType => 'Custom function';
@override
String get noProcessingType => 'No processing required';
@override
String get ruleTypeRequired => 'Processing type is required';
+3
View File
@@ -206,6 +206,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get customFunctionType => 'Кастомная функция';
@override
String get noProcessingType => 'Не требует обработки';
@override
String get ruleTypeRequired => 'Тип обработки обязателен';
+1
View File
@@ -64,6 +64,7 @@
"ruleTypeLabel": "Тип обработки",
"regexpType": "Регулярное выражение",
"customFunctionType": "Кастомная функция",
"noProcessingType": "Не требует обработки",
"ruleTypeRequired": "Тип обработки обязателен",
"regexpPatternHint": "Шаблон регулярного выражения",
"regexpPatternRequired": "Шаблон обязателен",
+29 -6
View File
@@ -26,12 +26,35 @@ class SmsItemCubit extends Cubit<SmsItemState> {
emit(SmsItemProcessing());
try {
// Вызываем сервис для создания транзакции
await _smsTransactionService.createTransactionFromSms(message);
// Помечаем СМС как обработанное
final updatedMessage = message.copyWith(status: SmsStatus.processed);
await _smsService.markAsProcessed(message.id);
emit(SmsItemSuccess());
onProcessed(updatedMessage); // Передаем обновленное сообщение
final result = await _smsTransactionService.createTransactionFromSms(message);
switch (result) {
case SmsProcessingResult.success:
// Транзакция успешно создана - помечаем СМС как обработанное
final updatedMessage = message.copyWith(status: SmsStatus.processed);
await _smsService.markAsProcessed(message.id);
emit(SmsItemSuccess());
onProcessed(updatedMessage);
break;
case SmsProcessingResult.ignored:
// SMS должно быть проигнорировано - помечаем как ignored
final updatedMessage = message.copyWith(status: SmsStatus.ignored);
await _smsService.markAsIgnored(message.id);
emit(SmsItemIgnored());
onProcessed(updatedMessage);
break;
case SmsProcessingResult.noRule:
// Правило не найдено - показываем ошибку и не меняем статус
emit(SmsItemError('Правило обработки не настроено для отправителя: ${message.sender}'));
break;
case SmsProcessingResult.error:
// Ошибка обработки - показываем ошибку и не меняем статус
emit(SmsItemError('Ошибка при обработке SMS'));
break;
}
} catch (e) {
emit(SmsItemError(e.toString()));
}
+9 -1
View File
@@ -15,6 +15,10 @@ enum SmsProcessingType {
/// Обработка с использованием кастомной функции.
@HiveField(1)
customFunction,
/// Не требует обработки.
@HiveField(2)
noProcessing,
}
/// Модель для хранения правила обработки СМС от конкретного отправителя.
@@ -43,7 +47,8 @@ class SmsProcessingRule extends HiveObject {
this.customFunctionId,
}) : assert(
(type == SmsProcessingType.regexp && pattern != null) ||
(type == SmsProcessingType.customFunction && customFunctionId != null),
(type == SmsProcessingType.customFunction && customFunctionId != null) ||
(type == SmsProcessingType.noProcessing),
'Pattern must be provided for regexp type, and customFunctionId for customFunction type.',
), id = id ?? IdGenerator.generateId();
@@ -64,6 +69,9 @@ class SmsProcessingRule extends HiveObject {
} else if (newType == SmsProcessingType.customFunction) {
newCustomFunctionId = customFunctionId ?? this.customFunctionId;
newPattern = null; // Сбрасываем pattern для customFunction
} else if (newType == SmsProcessingType.noProcessing) {
newPattern = null; // Сбрасываем pattern для noProcessing
newCustomFunctionId = null; // Сбрасываем customFunctionId для noProcessing
}
return SmsProcessingRule(
+4
View File
@@ -97,6 +97,8 @@ class SmsProcessingTypeAdapter extends TypeAdapter<SmsProcessingType> {
return SmsProcessingType.regexp;
case 1:
return SmsProcessingType.customFunction;
case 2:
return SmsProcessingType.noProcessing;
default:
return SmsProcessingType.regexp;
}
@@ -109,6 +111,8 @@ class SmsProcessingTypeAdapter extends TypeAdapter<SmsProcessingType> {
writer.writeByte(0);
case SmsProcessingType.customFunction:
writer.writeByte(1);
case SmsProcessingType.noProcessing:
writer.writeByte(2);
}
}
+115 -56
View File
@@ -12,20 +12,9 @@ class SmsMessageWidget extends StatelessWidget {
required this.message,
});
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is SmsMessageWidget &&
runtimeType == other.runtimeType &&
message == other.message;
@override
int get hashCode => message.hashCode;
@override
Widget build(BuildContext context) {
final cubit = context.read<SmsItemCubit>();
return BlocBuilder<SmsItemCubit, SmsItemState>(
builder: (context, state) {
return Card(
@@ -37,7 +26,7 @@ class SmsMessageWidget extends StatelessWidget {
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
gradient: _getGradientForStatus(message.status),
gradient: _getGradientForStatus(message.status, context),
),
child: Padding(
padding: const EdgeInsets.all(16),
@@ -53,10 +42,14 @@ class SmsMessageWidget extends StatelessWidget {
children: [
Row(
children: [
Icon(
Icons.account_circle,
color: Colors.grey[600],
size: 20,
Builder(
builder: (context) => Icon(
Icons.account_circle,
color: Theme.of(context).brightness == Brightness.dark
? Colors.grey[400]
: Colors.grey[600],
size: 20,
),
),
const SizedBox(width: 8),
Expanded(
@@ -71,26 +64,30 @@ class SmsMessageWidget extends StatelessWidget {
],
),
const SizedBox(height: 8),
Text(
message.body ?? '',
style: TextStyle(
color: Colors.grey[700],
fontSize: 14,
Builder(
builder: (context) => Text(
message.body ?? '',
style: TextStyle(
color: Theme.of(context).brightness == Brightness.dark
? Colors.grey[300]
: Colors.grey[700],
fontSize: 14,
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
],
),
),
const SizedBox(width: 8),
_buildStatusIndicator(message.status),
_buildStatusIndicator(message.status, context),
const SizedBox(width: 8),
_buildMenuButton(context, message, state),
],
),
const SizedBox(height: 12),
_buildBottomRow(message, state),
_buildBottomRow(message, state, context),
],
),
),
@@ -100,29 +97,40 @@ class SmsMessageWidget extends StatelessWidget {
);
}
LinearGradient _getGradientForStatus(SmsStatus status) {
LinearGradient _getGradientForStatus(SmsStatus status, BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
switch (status) {
case SmsStatus.pending:
return LinearGradient(
colors: [
Colors.blue.shade50,
Colors.blue.shade100,
colors: isDark ? [
Colors.grey.shade800,
Colors.grey.shade700,
] : [
Colors.grey.shade50,
Colors.grey.shade100,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
case SmsStatus.processed:
return LinearGradient(
colors: [
Colors.green.shade50,
Colors.green.shade100,
colors: isDark ? [
Colors.grey.shade900,
Colors.grey.shade800,
] : [
Colors.grey.shade200,
Colors.grey.shade300,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
case SmsStatus.ignored:
return LinearGradient(
colors: [
colors: isDark ? [
Colors.grey.shade900,
Colors.grey.shade800,
] : [
Colors.grey.shade100,
Colors.grey.shade200,
],
@@ -131,9 +139,12 @@ class SmsMessageWidget extends StatelessWidget {
);
case SmsStatus.error:
return LinearGradient(
colors: [
Colors.red.shade50,
Colors.red.shade100,
colors: isDark ? [
Colors.grey.shade800,
Colors.grey.shade700,
] : [
Colors.grey.shade100,
Colors.grey.shade200,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
@@ -141,29 +152,30 @@ class SmsMessageWidget extends StatelessWidget {
}
}
Widget _buildStatusIndicator(SmsStatus status) {
Widget _buildStatusIndicator(SmsStatus status, BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
Color color;
IconData icon;
String label;
switch (status) {
case SmsStatus.pending:
color = Colors.blue;
color = isDark ? Colors.grey.shade400 : Colors.grey.shade600;
icon = Icons.access_time;
label = 'Ожидает';
break;
case SmsStatus.processed:
color = Colors.green;
color = isDark ? Colors.grey.shade300 : Colors.black87;
icon = Icons.check_circle;
label = 'Обработано';
break;
case SmsStatus.ignored:
color = Colors.grey;
color = isDark ? Colors.grey.shade500 : Colors.grey.shade700;
icon = Icons.block;
label = 'Игнорировать';
break;
case SmsStatus.error:
color = Colors.red;
color = isDark ? Colors.grey.shade400 : Colors.grey.shade600;
icon = Icons.error;
label = 'Ошибка';
break;
@@ -194,24 +206,71 @@ class SmsMessageWidget extends StatelessWidget {
);
}
Widget _buildBottomRow(SmsMessage message, SmsItemState state) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
Widget _buildBottomRow(SmsMessage message, SmsItemState state, BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (message.date != null)
Text(
_formatDate(message.date!),
style: TextStyle(
color: Colors.grey[600],
fontSize: 12,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (message.date != null)
Text(
_formatDate(message.date!),
style: TextStyle(
color: Theme.of(context).brightness == Brightness.dark
? Colors.grey[400]
: Colors.grey[600],
fontSize: 12,
),
),
if (state is SmsItemProcessing)
const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
),
],
),
if (state is SmsItemError) ...[
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(context).brightness == Brightness.dark
? Colors.grey.shade800
: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Theme.of(context).brightness == Brightness.dark
? Colors.grey.shade600
: Colors.grey.shade400,
),
),
child: Row(
children: [
Icon(
Icons.error_outline,
color: Theme.of(context).brightness == Brightness.dark
? Colors.grey.shade400
: Colors.grey.shade700,
size: 16,
),
const SizedBox(width: 8),
Expanded(
child: Text(
state.error,
style: TextStyle(
color: Theme.of(context).brightness == Brightness.dark
? Colors.grey.shade300
: Colors.grey.shade700,
fontSize: 12,
),
),
),
],
),
),
if (state is SmsItemProcessing)
const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
),
],
],
);
}
+11 -3
View File
@@ -143,7 +143,9 @@ class _SmsSettingsDialogViewState extends State<_SmsSettingsDialogView> {
child: Text(
type == SmsProcessingType.regexp
? loc.regexpType
: loc.customFunctionType,
: type == SmsProcessingType.customFunction
? loc.customFunctionType
: loc.noProcessingType,
),
);
}).toList(),
@@ -171,6 +173,12 @@ class _SmsSettingsDialogViewState extends State<_SmsSettingsDialogView> {
context.read<SmsSettingsCubit>().updateRule(
rule.copyWith(type: type, pattern: currentPattern),
);
} else if (type == SmsProcessingType.noProcessing) {
// Очищаем контроллер при переключении на noProcessing
_patternController.clear();
context.read<SmsSettingsCubit>().updateRule(
rule.copyWith(type: type),
);
} else {
context.read<SmsSettingsCubit>().updateRule(
rule.copyWith(type: type),
@@ -199,7 +207,7 @@ class _SmsSettingsDialogViewState extends State<_SmsSettingsDialogView> {
);
},
validator: (value) {
if (value == null || value.isEmpty) {
if (rule.type == SmsProcessingType.regexp && (value == null || value.isEmpty)) {
return loc.regexpPatternRequired;
}
return null;
@@ -224,7 +232,7 @@ class _SmsSettingsDialogViewState extends State<_SmsSettingsDialogView> {
}
},
validator: (value) {
if (value == null || value.isEmpty) {
if (rule.type == SmsProcessingType.customFunction && (value == null || value.isEmpty)) {
return loc.customFunctionIdRequired;
}
return null;
+29 -10
View File
@@ -5,6 +5,14 @@ import 'package:budget_app/models/sms_message.dart';
import 'package:budget_app/services/custom_sms_functions.dart';
import 'package:logger/logger.dart';
/// Результат обработки SMS сообщения
enum SmsProcessingResult {
success, // Транзакция успешно создана
ignored, // SMS должно быть проигнорировано (noProcessing)
noRule, // Правило не найдено для данного отправителя
error, // Ошибка при обработке
}
/// Сервис для обработки транзакций из SMS сообщений
class SmsTransactionService {
final ISmsHandlerRepository _smsHandlerRepository;
@@ -16,15 +24,26 @@ class SmsTransactionService {
this._transactionBloc,
);
Future<void> createTransactionFromSms(
Future<SmsProcessingResult> createTransactionFromSms(
SmsMessage message,
) async {
try {
final settings = await _smsHandlerRepository.getSmsHandlerSettings();
final rule = settings?.rulesBySender[message.sender];
if (rule != null &&
rule.type == SmsProcessingType.customFunction &&
if (rule == null) {
// Правило не найдено для данного отправителя
_logger.w('Правило для создания транзакции не найдено для сендера: ${message.sender}');
return SmsProcessingResult.noRule;
}
if (rule.type == SmsProcessingType.noProcessing) {
// SMS должно быть проигнорировано
_logger.i('SMS от ${message.sender} помечено как не требующее обработки');
return SmsProcessingResult.ignored;
}
if (rule.type == SmsProcessingType.customFunction &&
rule.customFunctionId != null) {
final processingFunction = CustomSmsFunctions.getFunctionById(
rule.customFunctionId!,
@@ -39,24 +58,24 @@ class SmsTransactionService {
_logger.i('Creating transaction from SMS: ${message.id}');
_logger.d('Transaction details: ${transaction.toMap()}');
// Статус будет обновлен в SmsItemCubit
_logger.i('Transaction created successfully for SMS: ${message.id}');
return SmsProcessingResult.success;
} else {
throw Exception('Не удалось создать транзакцию');
}
} else {
throw Exception('Функция обработки не найдена для id: ${rule.customFunctionId}');
}
} else if (rule.type == SmsProcessingType.regexp) {
// TODO: Implement regexp processing
_logger.w('Regexp processing not yet implemented');
throw Exception('Regexp processing not yet implemented');
} else {
// Если правило не найдено, просто ничего не делаем.
// Логика игнорирования находится в SmsItemCubit
_logger.w('Правило для создания транзакции не найдено для сендера: ${message.sender}');
throw Exception('Неизвестный тип обработки: ${rule.type}');
}
} catch (e) {
_logger.e('Ошибка создания транзакции: $e');
// Перебрасываем ошибку, чтобы ее обработал SmsItemCubit
rethrow;
return SmsProcessingResult.error;
}
}
}