Fix ai add

This commit is contained in:
2025-07-19 17:53:37 +03:00
parent a7a9c9504e
commit 4bd509b561
6 changed files with 122 additions and 304 deletions
@@ -84,10 +84,10 @@ class HiveAiRuleRepository implements IAiRuleRepository {
Future<List<AiRule>> getByPriority() async {
try {
final rules = _box.values.toList();
rules.sort((a, b) => b.priority.compareTo(a.priority));
rules.sort((a, b) => b.updatedAt.compareTo(a.updatedAt));
return rules;
} catch (e) {
throw Exception('Ошибка получения правил ИИ по приоритету: $e');
throw Exception('Ошибка получения правил ИИ: $e');
}
}
-4
View File
@@ -224,10 +224,6 @@ class AiRulesBloc extends Bloc<AiRulesEvent, AiRulesState> {
errors.add('Название правила не может быть пустым');
}
// Валидация описания
if (event.rule.description.trim().isEmpty) {
errors.add('Описание правила не может быть пустым');
}
// Валидация процента уверенности
if (event.rule.confidencePercentage < 0 || event.rule.confidencePercentage > 100) {
+9 -36
View File
@@ -47,66 +47,51 @@ class AiRule extends Equatable {
@HiveField(1)
final String name;
/// Описание правила
@HiveField(2)
final String description;
/// Тип правила
@HiveField(3)
@HiveField(2)
final AiRuleType type;
/// Активность правила
@HiveField(4)
@HiveField(3)
final bool isActive;
/// Приоритет выполнения (чем больше, тем выше приоритет)
@HiveField(5)
final int priority;
/// Процент уверенности (0-100)
@HiveField(6)
@HiveField(4)
final int confidencePercentage;
/// Ссылка на сообщение
@HiveField(7)
final String? messageReference;
/// Статус обработки
@HiveField(8)
@HiveField(5)
final ProcessingStatus processingStatus;
// Поля для точки продаж
/// Паттерн для определения торговой точки (регулярное выражение)
@HiveField(9)
@HiveField(6)
final String? merchantPattern;
/// ID категории для автоматического назначения
@HiveField(10)
@HiveField(7)
final String? categoryId;
// Поля для шаблона пропуска
/// Регулярное выражение для пропуска сообщений
@HiveField(11)
@HiveField(8)
final String? skipRegex;
/// Дата создания
@HiveField(12)
@HiveField(9)
final DateTime createdAt;
/// Дата последнего обновления
@HiveField(13)
@HiveField(10)
final DateTime updatedAt;
/// Конструктор
AiRule({
String? id,
required this.name,
required this.description,
required this.type,
this.isActive = true,
this.priority = 0,
this.confidencePercentage = 80,
this.messageReference,
this.processingStatus = ProcessingStatus.created,
this.merchantPattern,
this.categoryId,
@@ -123,12 +108,9 @@ class AiRule extends Equatable {
return {
'id': id,
'name': name,
'description': description,
'type': type.name,
'isActive': isActive,
'priority': priority,
'confidencePercentage': confidencePercentage,
'messageReference': messageReference,
'processingStatus': processingStatus.name,
'merchantPattern': merchantPattern,
'categoryId': categoryId,
@@ -143,12 +125,9 @@ class AiRule extends Equatable {
return AiRule(
id: map['id'],
name: map['name'],
description: map['description'],
type: AiRuleType.values.firstWhere((e) => e.name == map['type']),
isActive: map['isActive'],
priority: map['priority'],
confidencePercentage: map['confidencePercentage'],
messageReference: map['messageReference'],
processingStatus: ProcessingStatus.values.firstWhere((e) => e.name == map['processingStatus']),
merchantPattern: map['merchantPattern'],
categoryId: map['categoryId'],
@@ -161,12 +140,9 @@ class AiRule extends Equatable {
/// Метод для создания копии объекта с возможностью изменения полей
AiRule copyWith({
String? name,
String? description,
AiRuleType? type,
bool? isActive,
int? priority,
int? confidencePercentage,
String? messageReference,
ProcessingStatus? processingStatus,
String? merchantPattern,
String? categoryId,
@@ -175,12 +151,9 @@ class AiRule extends Equatable {
return AiRule(
id: id,
name: name ?? this.name,
description: description ?? this.description,
type: type ?? this.type,
isActive: isActive ?? this.isActive,
priority: priority ?? this.priority,
confidencePercentage: confidencePercentage ?? this.confidencePercentage,
messageReference: messageReference ?? this.messageReference,
processingStatus: processingStatus ?? this.processingStatus,
merchantPattern: merchantPattern ?? this.merchantPattern,
categoryId: categoryId ?? this.categoryId,
+19 -28
View File
@@ -19,54 +19,45 @@ class AiRuleAdapter extends TypeAdapter<AiRule> {
return AiRule(
id: fields[0] as String?,
name: fields[1] as String,
description: fields[2] as String,
type: fields[3] as AiRuleType,
isActive: fields[4] == null ? true : fields[4] as bool,
priority: fields[5] == null ? 0 : (fields[5] as num).toInt(),
confidencePercentage: fields[6] == null ? 80 : (fields[6] as num).toInt(),
messageReference: fields[7] as String?,
processingStatus: fields[8] == null
type: fields[2] as AiRuleType,
isActive: fields[3] == null ? true : fields[3] as bool,
confidencePercentage: fields[4] == null ? 80 : (fields[4] as num).toInt(),
processingStatus: fields[5] == null
? ProcessingStatus.created
: fields[8] as ProcessingStatus,
merchantPattern: fields[9] as String?,
categoryId: fields[10] as String?,
skipRegex: fields[11] as String?,
createdAt: fields[12] as DateTime?,
updatedAt: fields[13] as DateTime?,
: fields[5] as ProcessingStatus,
merchantPattern: fields[6] as String?,
categoryId: fields[7] as String?,
skipRegex: fields[8] as String?,
createdAt: fields[9] as DateTime?,
updatedAt: fields[10] as DateTime?,
);
}
@override
void write(BinaryWriter writer, AiRule obj) {
writer
..writeByte(14)
..writeByte(11)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.name)
..writeByte(2)
..write(obj.description)
..writeByte(3)
..write(obj.type)
..writeByte(4)
..writeByte(3)
..write(obj.isActive)
..writeByte(5)
..write(obj.priority)
..writeByte(6)
..writeByte(4)
..write(obj.confidencePercentage)
..writeByte(7)
..write(obj.messageReference)
..writeByte(8)
..writeByte(5)
..write(obj.processingStatus)
..writeByte(9)
..writeByte(6)
..write(obj.merchantPattern)
..writeByte(10)
..writeByte(7)
..write(obj.categoryId)
..writeByte(11)
..writeByte(8)
..write(obj.skipRegex)
..writeByte(12)
..writeByte(9)
..write(obj.createdAt)
..writeByte(13)
..writeByte(10)
..write(obj.updatedAt);
}
@@ -22,16 +22,12 @@ class _AiRuleEditDialogState extends State<AiRuleEditDialog> {
final _formKey = GlobalKey<FormState>();
late final TextEditingController _nameController;
late final TextEditingController _descriptionController;
late final TextEditingController _merchantPatternController;
late final TextEditingController _categoryIdController;
late final TextEditingController _skipRegexController;
late final TextEditingController _messageReferenceController;
late AiRuleType _selectedType;
late bool _isActive;
late int _priority;
late int _confidencePercentage;
late ProcessingStatus _processingStatus;
List<Category> _categories = [];
@@ -44,16 +40,12 @@ class _AiRuleEditDialogState extends State<AiRuleEditDialog> {
final rule = widget.rule;
_nameController = TextEditingController(text: rule?.name ?? '');
_descriptionController = TextEditingController(text: rule?.description ?? '');
_merchantPatternController = TextEditingController(text: rule?.merchantPattern ?? '');
_categoryIdController = TextEditingController(text: rule?.categoryId ?? '');
_skipRegexController = TextEditingController(text: rule?.skipRegex ?? '');
_messageReferenceController = TextEditingController(text: rule?.messageReference ?? '');
_selectedType = rule?.type ?? AiRuleType.pointOfSale;
_isActive = rule?.isActive ?? true;
_priority = rule?.priority ?? 0;
_confidencePercentage = rule?.confidencePercentage ?? 80;
_processingStatus = rule?.processingStatus ?? ProcessingStatus.created;
_loadCategories();
@@ -62,11 +54,9 @@ class _AiRuleEditDialogState extends State<AiRuleEditDialog> {
@override
void dispose() {
_nameController.dispose();
_descriptionController.dispose();
_merchantPatternController.dispose();
_categoryIdController.dispose();
_skipRegexController.dispose();
_messageReferenceController.dispose();
super.dispose();
}
@@ -247,42 +237,19 @@ class _AiRuleEditDialogState extends State<AiRuleEditDialog> {
}
Widget _buildBasicFields() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
labelText: 'Название правила *',
hintText: 'Например: Сбербанк - продуктовые',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Название не может быть пустым';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _descriptionController,
decoration: const InputDecoration(
labelText: 'Описание',
hintText: 'Подробное описание правила...',
border: OutlineInputBorder(),
),
maxLines: 3,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Описание не может быть пустым';
}
return null;
},
),
],
return TextFormField(
controller: _nameController,
decoration: const InputDecoration(
labelText: 'Название правила *',
hintText: 'Например: Сбербанк - продуктовые',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Название не может быть пустым';
}
return null;
},
);
}
@@ -299,54 +266,28 @@ class _AiRuleEditDialogState extends State<AiRuleEditDialog> {
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: RadioListTile<AiRuleType>(
SizedBox(
width: double.infinity,
child: SegmentedButton<AiRuleType>(
segments: const [
ButtonSegment<AiRuleType>(
value: AiRuleType.pointOfSale,
groupValue: _selectedType,
onChanged: (value) {
setState(() {
_selectedType = value!;
});
},
title: const Row(
children: [
Icon(Icons.store, size: 20, color: Colors.blue),
SizedBox(width: 8),
Text('Точка продаж'),
],
),
subtitle: const Text('Определение категории по месту покупки'),
dense: true,
icon: Icon(Icons.store, size: 18),
label: Text('Точка продаж'),
),
),
],
),
Row(
children: [
Expanded(
child: RadioListTile<AiRuleType>(
ButtonSegment<AiRuleType>(
value: AiRuleType.skipTemplate,
groupValue: _selectedType,
onChanged: (value) {
setState(() {
_selectedType = value!;
});
},
title: const Row(
children: [
Icon(Icons.block, size: 20, color: Colors.orange),
SizedBox(width: 8),
Text('Пропуск сообщений'),
],
),
subtitle: const Text('Игнорирование определенных SMS'),
dense: true,
icon: Icon(Icons.block, size: 18),
label: Text('Пропуск SMS'),
),
),
],
],
selected: {_selectedType},
onSelectionChanged: (Set<AiRuleType> newSelection) {
setState(() {
_selectedType = newSelection.first;
});
},
),
),
],
);
@@ -412,9 +353,38 @@ class _AiRuleEditDialogState extends State<AiRuleEditDialog> {
items: _categories.map<DropdownMenuItem<Category>>((Category category) {
return DropdownMenuItem<Category>(
value: category,
child: Text(
'${category.isIncome ? '💰' : '💳'} ${category.name}',
overflow: TextOverflow.ellipsis,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: category.color,
shape: BoxShape.circle,
),
child: Icon(
category.icon,
size: 12,
color: Colors.white,
),
),
const SizedBox(width: 8),
Flexible(
child: Text(
category.name,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
Text(
category.isIncome ? '' : '',
style: TextStyle(
color: category.isIncome ? Colors.green : Colors.red,
fontWeight: FontWeight.bold,
),
),
],
),
);
}).toList(),
@@ -496,64 +466,6 @@ class _AiRuleEditDialogState extends State<AiRuleEditDialog> {
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: TextFormField(
initialValue: _priority.toString(),
decoration: const InputDecoration(
labelText: 'Приоритет',
border: OutlineInputBorder(),
helperText: 'Чем выше, тем раньше применяется',
),
keyboardType: TextInputType.number,
onChanged: (value) {
_priority = int.tryParse(value) ?? 0;
},
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Приоритет обязателен';
}
final priority = int.tryParse(value);
if (priority == null) {
return 'Должно быть числом';
}
return null;
},
),
),
const SizedBox(width: 16),
Expanded(
child: TextFormField(
initialValue: _confidencePercentage.toString(),
decoration: const InputDecoration(
labelText: 'Уверенность (%)',
border: OutlineInputBorder(),
helperText: 'От 0 до 100',
),
keyboardType: TextInputType.number,
onChanged: (value) {
_confidencePercentage = int.tryParse(value) ?? 80;
},
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Уверенность обязательна';
}
final confidence = int.tryParse(value);
if (confidence == null || confidence < 0 || confidence > 100) {
return 'От 0 до 100';
}
return null;
},
),
),
],
),
const SizedBox(height: 16),
DropdownButtonFormField<ProcessingStatus>(
value: _processingStatus,
decoration: const InputDecoration(
@@ -589,17 +501,6 @@ class _AiRuleEditDialogState extends State<AiRuleEditDialog> {
}
},
),
const SizedBox(height: 16),
TextFormField(
controller: _messageReferenceController,
decoration: const InputDecoration(
labelText: 'Ссылка на сообщение',
hintText: 'Необязательно',
border: OutlineInputBorder(),
),
),
],
);
}
@@ -612,14 +513,8 @@ class _AiRuleEditDialogState extends State<AiRuleEditDialog> {
final rule = AiRule(
id: widget.rule?.id,
name: _nameController.text.trim(),
description: _descriptionController.text.trim(),
type: _selectedType,
isActive: _isActive,
priority: _priority,
confidencePercentage: _confidencePercentage,
messageReference: _messageReferenceController.text.trim().isEmpty
? null
: _messageReferenceController.text.trim(),
processingStatus: _processingStatus,
merchantPattern: _selectedType == AiRuleType.pointOfSale
? _merchantPatternController.text.trim()
@@ -53,18 +53,6 @@ class AiRuleListItem extends StatelessWidget {
const SizedBox(height: 8),
// Описание
if (rule.description.isNotEmpty) ...[
Text(
rule.description,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 12),
],
// Детали правила
_buildRuleDetails(context),
@@ -74,62 +62,37 @@ class AiRuleListItem extends StatelessWidget {
// Нижняя панель с действиями
Row(
children: [
// Приоритет и уверенность
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.priority_high,
size: 16,
color: Theme.of(context).colorScheme.primary,
// Уверенность (только для статусов "создано" и "требует внимания")
if (rule.processingStatus == ProcessingStatus.created ||
rule.processingStatus == ProcessingStatus.needsAttention) ...
[
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(8),
),
const SizedBox(width: 4),
Text(
'П: ${rule.priority}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.primary,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.percent,
size: 16,
color: Theme.of(context).colorScheme.secondary,
),
const SizedBox(width: 4),
Text(
'${rule.confidencePercentage}%',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.secondary,
),
),
],
),
],
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.percent,
size: 16,
color: Theme.of(context).colorScheme.secondary,
),
const SizedBox(width: 4),
Text(
'${rule.confidencePercentage}%',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.secondary,
),
),
],
),
),
),
],
const Spacer(),