Files
Finlytic/FinlyticApp/lib/features/admin/widgets/pipeline_settings_widget.dart
T

314 lines
12 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/network/api_client.dart';
import '../../../core/theme/app_theme.dart';
import '../repositories/admin_repository.dart';
import 'service_settings_form.dart';
class ServiceConfigMeta {
final String key;
final String displayName;
final String description;
final IconData icon;
final Color accentColor;
const ServiceConfigMeta({
required this.key,
required this.displayName,
required this.description,
required this.icon,
required this.accentColor,
});
}
class PipelineSettingsWidget extends StatefulWidget {
final ApiClient? apiClient;
final AdminRepository? repository;
const PipelineSettingsWidget({super.key, this.apiClient, this.repository});
@override
State<PipelineSettingsWidget> createState() => _PipelineSettingsWidgetState();
}
class _PipelineSettingsWidgetState extends State<PipelineSettingsWidget> {
late final AdminRepository _repository;
String _selectedServiceKey = 'FinlyticAssets';
bool _isLoading = false;
bool _isSaving = false;
static const List<ServiceConfigMeta> _services = [
ServiceConfigMeta(
key: 'FinlyticAssets',
displayName: 'Asset Katalog & Logos',
description: 'Verwaltet ISIN Asset-Stammdaten & Trade Republic Logo Fetcher',
icon: Icons.inventory_2_outlined,
accentColor: Color(0xFF00E5FF),
),
ServiceConfigMeta(
key: 'FinlyticNews',
displayName: 'News Scraper & AI',
description: 'RSS Web Scraper Intervall & Entwurf-Retention',
icon: Icons.newspaper_outlined,
accentColor: Color(0xFF3B82F6),
),
ServiceConfigMeta(
key: 'FinlyticTechnicalAnalysis',
displayName: 'Technische Analyse',
description: 'EMA/SMA Perioden, RSI Grenzwerte & Supertrend Multiplikator',
icon: Icons.show_chart_outlined,
accentColor: Color(0xFF8B5CF6),
),
ServiceConfigMeta(
key: 'FinlyticSentiment',
displayName: 'Sentiment NLP',
description: 'NLP Vertrauens-Schwellenwerte & Text-Batching',
icon: Icons.psychology_outlined,
accentColor: Color(0xFFEC4899),
),
ServiceConfigMeta(
key: 'FinlyticEngine',
displayName: 'Trading Engine',
description: 'Strategy Screener, Trade Lifecycle & Risikomanagement',
icon: Icons.candlestick_chart_outlined,
accentColor: Color(0xFF10B981),
),
ServiceConfigMeta(
key: 'FinlyticFundamentals',
displayName: 'Fundamentaldaten',
description: 'Cache TTL Dauer & Yahoo Finance Fallback',
icon: Icons.corporate_fare_outlined,
accentColor: Color(0xFF06B6D4),
),
ServiceConfigMeta(
key: 'FinlyticBot',
displayName: 'FinlyticBot (Paper)',
description: 'Alpaca Paper Trading, Risikomanagement & Sizing Engine',
icon: Icons.smart_toy_outlined,
accentColor: Color(0xFF10B981),
),
];
final Map<String, Map<String, TextEditingController>> _controllers = {
'FinlyticAssets': {
'TradeRepublicMaxRequestPageSize': TextEditingController(text: '100'),
'AssetUpdateTypeDelay': TextEditingController(text: '0'),
'BatchAssetUpdateDelay': TextEditingController(text: '5'),
},
'FinlyticNews': {
'ScrapingIntervalMinutes': TextEditingController(text: '15'),
'PollingFrequencyMinutes': TextEditingController(text: '15'),
'ArticleRetentionDays': TextEditingController(text: '90'),
'DefaultPageSize': TextEditingController(text: '20'),
},
'FinlyticTechnicalAnalysis': {
'EmaShortPeriod': TextEditingController(text: '20'),
'SmaMediumPeriod': TextEditingController(text: '50'),
'SmaLongPeriod': TextEditingController(text: '200'),
'RsiOverboughtLimit': TextEditingController(text: '70'),
'RsiOversoldLimit': TextEditingController(text: '30'),
'SupertrendMultiplier': TextEditingController(text: '3.0'),
},
'FinlyticSentiment': {
'MinConfidenceScore': TextEditingController(text: '0.70'),
'MaxBatchSize': TextEditingController(text: '50'),
},
'FinlyticEngine': {
'Engine.MinCompositeScore': TextEditingController(text: '75.0'),
'Engine.WeightTechnical': TextEditingController(text: '0.45'),
'Engine.WeightSentiment': TextEditingController(text: '0.35'),
'Engine.WeightFundamental': TextEditingController(text: '0.20'),
'Engine.EarningsLockoutDays': TextEditingController(text: '2'),
'Engine.MinDerivativeLeverage': TextEditingController(text: '5.0'),
'Engine.TargetDefaultLeverage': TextEditingController(text: '7.0'),
'Engine.KnockOutSafetyBufferPercent': TextEditingController(text: '2.0'),
'Engine.EnableAiValidation': TextEditingController(text: 'true'),
'Engine.EnablePaperTradingBot': TextEditingController(text: 'false'),
'Engine.PollingIntervalSeconds': TextEditingController(text: '120'),
'Engine.MonitoringIntervalSeconds': TextEditingController(text: '60'),
},
'FinlyticFundamentals': {
'CacheTtlHours': TextEditingController(text: '24'),
'EnableYahooFallback': TextEditingController(text: 'true'),
},
'FinlyticBot': {
'Alpaca.KeyId': TextEditingController(text: ''),
'Alpaca.SecretKey': TextEditingController(text: ''),
'Alpaca.IsPaper': TextEditingController(text: 'true'),
'Bot.EnableAutoExecution': TextEditingController(text: 'true'),
'Bot.RiskPerTradePercent': TextEditingController(text: '1.0'),
'Bot.MaxPositionAllocationPercent': TextEditingController(text: '20.0'),
'Bot.MaxConcurrentPositions': TextEditingController(text: '5'),
'Bot.DailyLossLimitPercent': TextEditingController(text: '3.0'),
'Bot.MonitoringIntervalSeconds': TextEditingController(text: '15'),
},
};
bool _initialized = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_initialized) {
_initialized = true;
final client = widget.apiClient ?? context.read<ApiClient>();
_repository = widget.repository ?? AdminRepository(apiClient: client);
_fetchSettings();
}
}
Future<void> _fetchSettings() async {
setState(() => _isLoading = true);
try {
final settings = await _repository.fetchSettings();
settings.forEach((svc, items) {
_controllers.putIfAbsent(svc, () => {});
for (final s in items) {
if (_controllers[svc]!.containsKey(s.key)) {
_controllers[svc]![s.key]!.text = s.value;
} else {
_controllers[svc]![s.key] = TextEditingController(text: s.value);
}
}
});
} catch (_) {
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
Future<void> _saveSettings() async {
setState(() => _isSaving = true);
try {
final currentSvcControllers = _controllers[_selectedServiceKey] ?? {};
final payload = <String, String>{};
currentSvcControllers.forEach((k, v) {
payload[k] = v.text;
});
await _repository.updateServiceSettings(_selectedServiceKey, payload);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.check_circle_outline, color: Colors.black),
const SizedBox(width: 8),
Expanded(
child: Text(
'Einstellungen für $_selectedServiceKey gespeichert & synchronisiert.',
style: const TextStyle(color: Colors.black, fontWeight: FontWeight.w600),
),
),
],
),
backgroundColor: AppTheme.primaryEmerald,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
);
}
} catch (ex) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Fehler beim Speichern: $ex'),
backgroundColor: AppTheme.accentRed,
behavior: SnackBarBehavior.floating,
),
);
}
} finally {
if (mounted) setState(() => _isSaving = false);
}
}
@override
Widget build(BuildContext context) {
final activeService = _services.firstWhere((s) => s.key == _selectedServiceKey, orElse: () => _services.first);
final activeControllers = _controllers[_selectedServiceKey] ?? {};
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: _services.map((svc) {
final isSelected = svc.key == _selectedServiceKey;
return Padding(
padding: const EdgeInsets.only(right: 8, bottom: 12),
child: InkWell(
onTap: () => setState(() => _selectedServiceKey = svc.key),
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: isSelected ? svc.accentColor.withValues(alpha: 0.15) : AppTheme.glassSurface,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected ? svc.accentColor : AppTheme.glassBorder,
width: isSelected ? 1.5 : 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(svc.icon, size: 16, color: isSelected ? svc.accentColor : AppTheme.textMuted),
const SizedBox(width: 8),
Text(
svc.displayName,
style: TextStyle(
color: isSelected ? Colors.white : AppTheme.textMuted,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
fontSize: 12,
),
),
],
),
),
),
);
}).toList(),
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(activeService.displayName, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white)),
const SizedBox(height: 2),
Text(activeService.description, style: TextStyle(fontSize: 12, color: AppTheme.textMuted)),
],
),
ElevatedButton.icon(
onPressed: _isSaving ? null : _saveSettings,
icon: _isSaving
? const SizedBox(width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.black))
: const Icon(Icons.save_outlined, size: 16),
label: Text(_isSaving ? 'Speichere...' : 'Speichern'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryEmerald,
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
),
),
],
),
const SizedBox(height: 14),
if (_isLoading)
Center(child: CircularProgressIndicator(color: AppTheme.primaryEmerald))
else
ServiceSettingsForm(
controllers: activeControllers,
accentColor: activeService.accentColor,
),
],
);
}
}