import 'package:flutter/material.dart'; import '../../../core/network/api_client.dart'; import '../../../core/theme/app_theme.dart'; import '../../../core/widgets/glass_container.dart'; import '../models/recent_setup_model.dart'; import '../models/watchlist_entry_model.dart'; import '../repositories/admin_repository.dart'; /// Card sitting next to "AUFSCHLÜSSELUNG NACH GRUND" showing how many assets /// FinlyticTechnicals' background scanner is currently watching. Tapping opens /// a dialog listing every entry — this directly answers "is anything even /// being checked in the background right now", independent of whether any of /// those checks have (yet) produced a proposal-worthy evaluation the engine /// history tab above would show. class WatchlistCard extends StatefulWidget { final ApiClient apiClient; const WatchlistCard({super.key, required this.apiClient}); @override State createState() => _WatchlistCardState(); } class _WatchlistCardState extends State { late final AdminRepository _repository = AdminRepository(apiClient: widget.apiClient); List? _entries; String? _error; @override void initState() { super.initState(); _load(); } Future _load() async { try { final entries = await _repository.fetchWatchlist(); if (!mounted) return; setState(() { _entries = entries; _error = null; }); } catch (e) { if (!mounted) return; setState(() => _error = e.toString()); } } void _showDialog() { showDialog( context: context, builder: (_) => _WatchlistDialog(repository: _repository, initialEntries: _entries ?? const []), ); } @override Widget build(BuildContext context) { final count = _entries?.length; final value = _error != null ? '—' : (count?.toString() ?? '…'); return GlassContainer( padding: const EdgeInsets.all(14), onTap: _showDialog, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text('WATCHLIST', style: TextStyle(color: AppTheme.textSecondary, fontSize: 11, fontWeight: FontWeight.w900, letterSpacing: 0.5)), const Spacer(), Icon(Icons.list_alt_rounded, size: 16, color: AppTheme.accentCyan), ], ), const SizedBox(height: 10), Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text(value, style: TextStyle(fontSize: 22, color: AppTheme.textPrimary, fontWeight: FontWeight.bold)), const SizedBox(width: 8), Padding( padding: const EdgeInsets.only(bottom: 4), child: Text('überwachte Assets', style: TextStyle(color: AppTheme.textMuted, fontSize: 12)), ), ], ), if (_error != null) ...[ const SizedBox(height: 6), Text(_error!, style: TextStyle(color: AppTheme.accentRed, fontSize: 11)), ], ], ), ); } } class _WatchlistDialog extends StatefulWidget { final AdminRepository repository; final List initialEntries; const _WatchlistDialog({required this.repository, required this.initialEntries}); @override State<_WatchlistDialog> createState() => _WatchlistDialogState(); } class _WatchlistDialogState extends State<_WatchlistDialog> { late List _entries = widget.initialEntries; bool _refreshing = false; Future _refresh() async { setState(() => _refreshing = true); try { final fresh = await widget.repository.fetchWatchlist(); if (!mounted) return; setState(() { _entries = fresh; _refreshing = false; }); } catch (_) { if (!mounted) return; setState(() => _refreshing = false); } } String _formatTimestamp(DateTime utc) { final local = utc.toLocal(); final d = local.day.toString().padLeft(2, '0'); final m = local.month.toString().padLeft(2, '0'); final h = local.hour.toString().padLeft(2, '0'); final min = local.minute.toString().padLeft(2, '0'); return '$d.$m.${local.year} $h:$min'; } @override Widget build(BuildContext context) { final activeTheme = AppTheme.activePreset; return Dialog( backgroundColor: activeTheme.cardSurface, insetPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 560, maxHeight: 640), child: GlassContainer( borderRadius: 20, padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Row( children: [ Icon(Icons.list_alt_rounded, color: AppTheme.accentCyan, size: 20), const SizedBox(width: 10), Expanded( child: Text('Watchlist (${_entries.length})', style: const TextStyle(color: Colors.white, fontSize: 17, fontWeight: FontWeight.bold)), ), IconButton( onPressed: _refreshing ? null : _refresh, icon: _refreshing ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)) : const Icon(Icons.refresh_rounded, color: Colors.white70), tooltip: 'Neu laden', ), IconButton( onPressed: () => Navigator.of(context).pop(), icon: const Icon(Icons.close_rounded, color: Colors.white70), ), ], ), Text( 'Assets, die FinlyticTechnicals derzeit im Hintergrund fortlaufend überprüft. Eintrag antippen für die letzten Bewertungen.', style: TextStyle(color: AppTheme.textMuted, fontSize: 12), ), const SizedBox(height: 12), if (_entries.isEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 24), child: Center( child: Text('Die Watchlist ist derzeit leer.', style: TextStyle(color: AppTheme.textMuted, fontSize: 13)), ), ) else Flexible( child: ListView.separated( shrinkWrap: true, itemCount: _entries.length, separatorBuilder: (_, __) => const Divider(height: 1, color: Colors.white12), itemBuilder: (context, index) => _WatchlistEntryTile( entry: _entries[index], repository: widget.repository, formatTimestamp: _formatTimestamp, ), ), ), ], ), ), ), ); } } class _WatchlistEntryTile extends StatefulWidget { final WatchlistEntryModel entry; final AdminRepository repository; final String Function(DateTime) formatTimestamp; const _WatchlistEntryTile({required this.entry, required this.repository, required this.formatTimestamp}); @override State<_WatchlistEntryTile> createState() => _WatchlistEntryTileState(); } class _WatchlistEntryTileState extends State<_WatchlistEntryTile> { List? _history; bool _loading = false; String? _error; Future _loadHistory() async { if (_history != null || _loading) return; setState(() => _loading = true); try { final history = await widget.repository.fetchWatchlistEntryHistory(widget.entry.isin); if (!mounted) return; setState(() { _history = history; _loading = false; }); } catch (e) { if (!mounted) return; setState(() { _error = e.toString(); _loading = false; }); } } @override Widget build(BuildContext context) { final entry = widget.entry; return Theme( data: Theme.of(context).copyWith(dividerColor: Colors.transparent), child: ExpansionTile( onExpansionChanged: (expanded) { if (expanded) _loadHistory(); }, tilePadding: EdgeInsets.zero, title: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( entry.symbol?.isNotEmpty == true ? entry.symbol! : entry.isin, style: TextStyle(color: AppTheme.textPrimary, fontWeight: FontWeight.bold, fontSize: 14), ), Text(entry.isin, style: TextStyle(color: AppTheme.textMuted, fontSize: 11)), ], ), ), if (entry.source != null) Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), margin: const EdgeInsets.only(right: 8), decoration: BoxDecoration( color: AppTheme.accentCyan.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(6), border: Border.all(color: AppTheme.accentCyan.withValues(alpha: 0.4)), ), child: Text(entry.source!.label, style: TextStyle(color: AppTheme.accentCyan, fontSize: 10, fontWeight: FontWeight.bold)), ), ], ), subtitle: Text( 'Seit ${widget.formatTimestamp(entry.addedAtUtc)}' '${entry.expiresAtUtc != null ? ' · Läuft ab ${widget.formatTimestamp(entry.expiresAtUtc!)}' : ''}', style: TextStyle(color: AppTheme.textMuted, fontSize: 11), ), children: [ Padding( padding: const EdgeInsets.only(bottom: 12), child: _buildHistoryBody(), ), ], ), ); } Widget _buildHistoryBody() { if (_loading) { return const Padding( padding: EdgeInsets.symmetric(vertical: 8), child: Center(child: SizedBox(width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2))), ); } if (_error != null) { return Text(_error!, style: TextStyle(color: AppTheme.accentRed, fontSize: 12)); } final history = _history ?? const []; if (history.isEmpty) { return Text( 'Noch keine technische Bewertung für dieses Asset erfasst.', style: TextStyle(color: AppTheme.textMuted, fontSize: 12), ); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('LETZTE BEWERTUNGEN', style: TextStyle(color: AppTheme.textSecondary, fontSize: 10, fontWeight: FontWeight.w900, letterSpacing: 0.5)), const SizedBox(height: 6), ...history.map((setup) => Padding( padding: const EdgeInsets.symmetric(vertical: 3), child: Row( children: [ SizedBox( width: 90, child: Text(widget.formatTimestamp(setup.createdAt), style: TextStyle(color: AppTheme.textMuted, fontSize: 11)), ), Expanded( child: Text(setup.strategyName, style: TextStyle(color: AppTheme.textPrimary, fontSize: 11), overflow: TextOverflow.ellipsis), ), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: (setup.isTopPick ? AppTheme.primaryEmerald : Colors.amber).withValues(alpha: 0.15), borderRadius: BorderRadius.circular(6), ), child: Text( setup.qualityScore.toStringAsFixed(1), style: TextStyle( color: setup.isTopPick ? AppTheme.primaryEmerald : Colors.amber, fontSize: 11, fontWeight: FontWeight.bold, ), ), ), ], ), )), ], ); } }