import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../core/theme/app_theme.dart'; import '../../../../core/widgets/glass_container.dart'; import '../../../../core/widgets/shimmer_loading.dart'; import '../../../../core/widgets/status_badge.dart'; import '../../bloc/fundamentals/asset_fundamentals_bloc.dart'; import '../../bloc/fundamentals/asset_fundamentals_event.dart'; import '../../bloc/fundamentals/asset_fundamentals_state.dart'; import '../../models/fundamental_data_model.dart'; import '../../utils/metric_explanations.dart'; class FundamentalsTab extends StatefulWidget { final String isin; final String? symbol; final bool isEmbedded; const FundamentalsTab({ super.key, this.symbol, this.isEmbedded = false, required this.isin, }); @override State createState() => _FundamentalsTabState(); } class _FundamentalsTabState extends State { String _sym = '\$'; String _curCode = 'USD'; @override void initState() { super.initState(); } @override void didUpdateWidget(covariant FundamentalsTab oldWidget) { super.didUpdateWidget(oldWidget); } @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { if (state is AssetFundamentalsLoading) { return _buildFundamentalsShimmer(context); } if (state is AssetFundamentalsError) { return Center( child: GlassContainer( padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.error_outline, color: AppTheme.accentRed, size: 48), const SizedBox(height: 12), Text('Fehler beim Laden der Fundamentaldaten: ${state.message}', style: const TextStyle(color: Colors.white70)), const SizedBox(height: 16), ElevatedButton.icon( onPressed: () => context.read().add(LoadAssetFundamentals(widget.isin, ticker: widget.symbol, forceRefresh: true)), icon: const Icon(Icons.refresh), label: const Text('Erneut versuchen'), ), ], ), ), ); } if (state is AssetFundamentalsLoaded) { final data = state.data; if (data != null) { _sym = _getCurrencySymbol(data.ticker); _curCode = _getCurrencyCode(data.ticker); } if (data == null) { return _buildEmptyState(); } return SingleChildScrollView( physics: widget.isEmbedded ? const NeverScrollableScrollPhysics() : null, padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 1. Analyst Forecasts & Price Targets Header Card _buildPriceTargetCard(data), const SizedBox(height: 20), // 2. Responsive Side-by-Side Category List Panels (Valuation, Profitability, Dividends) _buildCategoryPanels(data), const SizedBox(height: 20), // 3. Company Description & Detailed Executive Board _buildSectionHeader('Unternehmensprofil & Führungskräfte', Icons.business_outlined), const SizedBox(height: 12), _buildProfileSection(data), ], ), ); } return _buildEmptyState(); }, ); } Widget _buildPriceTargetCard(FundamentalDataModel data) { final rating = data.consensusRating ?? 'N/A'; final targetMean = data.priceTargetMean; final targetLow = data.priceTargetLow; final targetHigh = data.priceTargetHigh; return GlassContainer( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Icon(Icons.trending_up, color: AppTheme.primaryEmerald, size: 22), const SizedBox(width: 8), const Text('Analysten-Konsens & Kursziele', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white)), ], ), StatusBadge(label: rating.toUpperCase(), color: AppTheme.primaryEmerald), ], ), const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _buildTargetStat('Mindestkursziel', _fmtCurrency(targetLow), AppTheme.accentRed), _buildTargetStat('Konsens-Ziel (Durchschnitt)', _fmtCurrency(targetMean), AppTheme.primaryEmerald), _buildTargetStat('Höchstkursziel', _fmtCurrency(targetHigh), AppTheme.accentCyan), ], ), ], ), ); } Widget _buildTargetStat(String title, String val, Color col) { return Column( children: [ Text(title, style: TextStyle(color: AppTheme.textMuted, fontSize: 11)), const SizedBox(height: 4), Text(val, style: TextStyle(color: col, fontWeight: FontWeight.bold, fontSize: 16)), ], ); } Widget _buildFundamentalsShimmer(BuildContext context) { final isDesktop = MediaQuery.of(context).size.width >= 1050; final isTablet = MediaQuery.of(context).size.width >= 680 && MediaQuery.of(context).size.width < 1050; Widget panelShimmer() { return GlassContainer( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const ShimmerLoading(width: 180, height: 18, borderRadius: 6), const SizedBox(height: 12), const Divider(color: Colors.white10, height: 1), const SizedBox(height: 8), for (int i = 0; i < 9; i++) ...[ Padding( padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 4), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: const [ ShimmerLoading(width: 100, height: 14, borderRadius: 4), ShimmerLoading(width: 60, height: 14, borderRadius: 4), ], ), ), ], ], ), ); } return SingleChildScrollView( physics: widget.isEmbedded ? const NeverScrollableScrollPhysics() : null, padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Price Target Card Shimmer const ShimmerLoading(width: double.infinity, height: 86, borderRadius: 16), const SizedBox(height: 20), // 3 Category Panels Shimmer if (isDesktop) Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: panelShimmer()), const SizedBox(width: 14), Expanded(child: panelShimmer()), const SizedBox(width: 14), Expanded(child: panelShimmer()), ], ) else if (isTablet) Column( children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: panelShimmer()), const SizedBox(width: 12), Expanded(child: panelShimmer()), ], ), const SizedBox(height: 12), panelShimmer(), ], ) else Column( children: [ panelShimmer(), const SizedBox(height: 12), panelShimmer(), const SizedBox(height: 12), panelShimmer(), ], ), const SizedBox(height: 20), // Profile Section Shimmer const ShimmerLoading(width: 220, height: 20, borderRadius: 6), const SizedBox(height: 12), const ShimmerLoading(width: double.infinity, height: 140, borderRadius: 16), ], ), ); } Widget _buildProfileSection(FundamentalDataModel data) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ GlassContainer( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (data.sector != null || data.industry != null || data.country != null) ...[ Row( children: [ if (data.sector != null) ...[ _buildProfileBadge(data.sector!, Icons.category_outlined), const SizedBox(width: 8), ], if (data.country != null) _buildProfileBadge(data.country!, Icons.place_outlined), ], ), const SizedBox(height: 12), ], Text( data.businessSummary != null && data.businessSummary!.isNotEmpty ? data.businessSummary! : 'Keine Beschreibung für dieses Asset verfügbar.', style: const TextStyle(color: Colors.white70, height: 1.5, fontSize: 13), ), if (data.employees != null) ...[ const SizedBox(height: 12), Row( children: [ Icon(Icons.people_outline, size: 16, color: AppTheme.textMuted), const SizedBox(width: 6), Text( 'Mitarbeiter: ${data.employees}', style: TextStyle(color: AppTheme.textMuted, fontSize: 12), ), ], ), ], ], ), ), if (data.executives.isNotEmpty) ...[ const SizedBox(height: 16), const Text('Führungskräfte (Board)', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.white, fontSize: 14)), const SizedBox(height: 8), ...data.executives.take(5).map((e) => Padding( padding: const EdgeInsets.only(bottom: 8), child: GlassContainer( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), child: Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: AppTheme.primaryEmerald.withValues(alpha: 0.1), shape: BoxShape.circle, ), child: Icon(Icons.person_outline, color: AppTheme.primaryEmerald, size: 18), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(e.name, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: Colors.white)), Text(e.title, style: TextStyle(color: AppTheme.textMuted, fontSize: 11)), ], ), ), if (e.compensation != null && e.compensation! > 0) Text( _formatNumber(e.compensation), style: TextStyle(color: AppTheme.accentCyan, fontWeight: FontWeight.bold, fontSize: 12), ), ], ), ), )), ], ], ); } Widget _buildProfileBadge(String label, IconData icon) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.white10), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 12, color: AppTheme.primaryEmerald), const SizedBox(width: 6), Text(label, style: const TextStyle(color: Colors.white70, fontSize: 11)), ], ), ); } Widget _buildEmptyState() { return Center( child: GlassContainer( padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.insert_chart_outlined, color: AppTheme.textMuted, size: 48), const SizedBox(height: 12), const Text('Keine Fundamentaldaten verfügbar.', style: TextStyle(color: Colors.white70, fontWeight: FontWeight.bold)), const SizedBox(height: 6), Text('Für dieses Asset wurden noch keine Bilanz- oder Bewertungskennzahlen erfasst.', style: TextStyle(color: AppTheme.textMuted, fontSize: 12), textAlign: TextAlign.center), const SizedBox(height: 16), ElevatedButton.icon( onPressed: () => context.read().add(LoadAssetFundamentals(widget.isin, ticker: widget.symbol, forceRefresh: true)), icon: const Icon(Icons.download), label: const Text('Daten von Backend abrufen'), ), ], ), ), ); } Widget _buildSectionHeader(String title, IconData icon) { return Row( children: [ Icon(icon, color: AppTheme.primaryEmerald, size: 20), const SizedBox(width: 8), Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white)), ], ); } Widget _buildCategoryPanels(FundamentalDataModel data) { final valuationItems = [ _MetricRowItem('KGV (Trailing P/E)', _fmtMultiple(data.peRatioTrailing)), _MetricRowItem('KGV (Forward P/E)', _fmtMultiple(data.peRatioForward)), _MetricRowItem('PEG Ratio', _fmtMultiple(data.pegRatio)), _MetricRowItem('KBV (P/B Ratio)', _fmtMultiple(data.pbRatio)), _MetricRowItem('KUV (P/S Ratio)', _fmtMultiple(data.psRatio)), _MetricRowItem('EV / EBITDA', _fmtMultiple(data.evToEbitda)), _MetricRowItem('EV / Sales', _fmtMultiple(data.evToRevenue)), _MetricRowItem('Enterprise Value', _formatNumber(data.enterpriseValue)), _MetricRowItem('Marktkapitalisierung', _formatNumber(data.marketCapitalization)), _MetricRowItem('Gewinn je Aktie (EPS)', _fmtCurrency(data.dilutedEps)), _MetricRowItem('52W Höchststand', _fmtCurrency(data.fiftyTwoWeekHigh)), _MetricRowItem('52W Tiefststand', _fmtCurrency(data.fiftyTwoWeekLow)), ]; final profitabilityItems = [ _MetricRowItem('Umsatzerlöse (Revenue)', _formatNumber(data.totalRevenue)), _MetricRowItem('Umsatzwachstum (YoY)', _fmtPercent(data.revenueGrowthYoY)), _MetricRowItem('Bruttogewinn', _formatNumber(data.grossProfit)), _MetricRowItem('Bruttomarge (Gross)', _fmtPercent(data.grossMargin)), _MetricRowItem('EBITDA', _formatNumber(data.ebitda)), _MetricRowItem('Operative Marge', _fmtPercent(data.operatingMargin)), _MetricRowItem('Nettogewinnmarge', _fmtPercent(data.netProfitMargin)), _MetricRowItem('Eigenkapitalrendite (ROE)', _fmtPercent(data.returnOnEquity)), _MetricRowItem('Gesamtkapitalrendite (ROA)', _fmtPercent(data.returnOnAssets)), _MetricRowItem('Verschuldungsgrad (D/E)', _fmtDebtToEquity(data.debtToEquity)), _MetricRowItem('Current Ratio', _fmtMultiple(data.currentRatio)), _MetricRowItem('Liquide Mittel (Cash)', _formatNumber(data.totalCash)), _MetricRowItem('Gesamtverschuldung (Debt)', _formatNumber(data.totalDebt)), _MetricRowItem('Operativer Cashflow', _formatNumber(data.operatingCashFlow)), _MetricRowItem('Free Cashflow', _formatNumber(data.freeCashFlow)), ]; final dividendItems = [ _MetricRowItem('Dividendenrendite', _fmtPercent(data.dividendYield)), _MetricRowItem('Ausschüttungsquote (Payout)', _fmtPercent(data.payoutRatio)), _MetricRowItem('Ex-Dividendentag', _fmtDate(data.exDividendDate)), _MetricRowItem('Nächste Quartalszahlen', _fmtDate(data.nextEarningsDate)), _MetricRowItem('Konsens-Rating', data.consensusRating != null ? data.consensusRating!.toUpperCase() : 'N/A'), _MetricRowItem('Institutioneller Anteil', _fmtPercent(data.percentHeldByInstitutions)), _MetricRowItem('Insider Anteil', _fmtPercent(data.percentHeldByInsiders)), _MetricRowItem('Short % of Float', _fmtPercent(data.shortPercentOfFloat)), ]; final panel1 = _buildCategoryPanel( title: 'Bewertungskennzahlen & Multiples', icon: Icons.analytics_outlined, items: valuationItems, ); final panel2 = _buildCategoryPanel( title: 'Rentabilität & Finanzen', icon: Icons.account_balance_outlined, items: profitabilityItems, ); final panel3 = _buildCategoryPanel( title: 'Dividenden & Termine', icon: Icons.pie_chart_outline, items: dividendItems, ); return LayoutBuilder( builder: (context, constraints) { if (constraints.maxWidth >= 1050) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: panel1), const SizedBox(width: 14), Expanded(child: panel2), const SizedBox(width: 14), Expanded(child: panel3), ], ); } else if (constraints.maxWidth >= 680) { return Column( children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: panel1), const SizedBox(width: 12), Expanded(child: panel2), ], ), const SizedBox(height: 12), panel3, ], ); } else { return Column( children: [ panel1, const SizedBox(height: 12), panel2, const SizedBox(height: 12), panel3, ], ); } }, ); } Widget _buildCategoryPanel({ required String title, required IconData icon, required List<_MetricRowItem> items, }) { return GlassContainer( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: AppTheme.primaryEmerald.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(6), ), child: Icon(icon, color: AppTheme.primaryEmerald, size: 16), ), const SizedBox(width: 8), Expanded( child: Text( title, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.bold, color: Colors.white, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), const SizedBox(height: 8), const Divider(color: Colors.white10, height: 1), const SizedBox(height: 4), ...items.asMap().entries.map((entry) { final idx = entry.key; final item = entry.value; final isEven = idx % 2 == 0; return _buildMetricListRow(item.label, item.value, isEven: isEven, valueColor: item.valueColor); }), ], ), ); } Widget _buildMetricListRow(String label, String value, {bool isEven = false, Color? valueColor}) { return InkWell( onTap: () => MetricExplanations.show(context, label), borderRadius: BorderRadius.circular(6), child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), decoration: BoxDecoration( color: isEven ? Colors.white.withValues(alpha: 0.02) : Colors.transparent, borderRadius: BorderRadius.circular(6), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( mainAxisSize: MainAxisSize.min, children: [ Text( label, style: TextStyle(color: AppTheme.textMuted, fontSize: 12), ), const SizedBox(width: 4), Icon(Icons.info_outline, size: 11, color: AppTheme.textMuted.withValues(alpha: 0.6)), ], ), const SizedBox(width: 8), Flexible( child: Text( value, style: TextStyle( color: valueColor ?? (value == 'N/A' ? AppTheme.textMuted : Colors.white), fontWeight: FontWeight.w600, fontSize: 12, ), textAlign: TextAlign.right, maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), ), ); } String _fmtMultiple(dynamic val) { if (val == null) return 'N/A'; final n = (val is num) ? val.toDouble() : double.tryParse(val.toString()); return n != null ? '${n.toStringAsFixed(2)}x' : 'N/A'; } String _fmtDays(dynamic val) { if (val == null) return 'N/A'; final n = (val is num) ? val.toDouble() : double.tryParse(val.toString()); return n != null ? '${n.toStringAsFixed(1)} Tage' : 'N/A'; } String _fmtDebtToEquity(dynamic val) { if (val == null) return 'N/A'; final n = (val is num) ? val.toDouble() : double.tryParse(val.toString()); if (n == null) return 'N/A'; // Yahoo liefert D/E als Prozentwert (z. B. 145.23 = 145.23% oder Faktor 1.45x) if (n > 5) { return '${(n / 100).toStringAsFixed(2)}x (${n.toStringAsFixed(1)} %)'; } return '${n.toStringAsFixed(2)}x (${(n * 100).toStringAsFixed(1)} %)'; } String _fmtPercent(dynamic val) { if (val == null) return 'N/A'; final n = (val is num) ? val.toDouble() : double.tryParse(val.toString()); if (n == null) return 'N/A'; // Yahoo liefert Margen/Renditen als Dezimalzahl (z. B. 0.25 = 25%, 1.2 = 120%) // Wenn |n| <= 2.5 ist, handelt es sich um eine Dezimalquote -> mit 100 multiplizieren final p = n.abs() <= 2.5 ? n * 100 : n; return '${p.toStringAsFixed(2)} %'; } String _fmtCurrency(dynamic val) { if (val == null) return 'N/A'; final n = (val is num) ? val.toDouble() : double.tryParse(val.toString()); if (n == null || n == 0) return 'N/A'; return '$_sym${n.toStringAsFixed(2)}'; } String _fmtDate(dynamic val) { if (val == null) return 'N/A'; final dt = DateTime.tryParse(val.toString()); return dt != null ? '${dt.day.toString().padLeft(2, '0')}.${dt.month.toString().padLeft(2, '0')}.${dt.year}' : val.toString(); } String _formatNumber(dynamic val) { if (val == null) return 'N/A'; final num? n = val is num ? val : num.tryParse(val.toString()); if (n == null) return val.toString(); final isNegative = n < 0; final absVal = n.abs(); final prefix = isNegative ? '-$_sym' : _sym; if (absVal >= 1e12) { return '$prefix${(absVal / 1e12).toStringAsFixed(2)} Bio.'; } else if (absVal >= 1e9) { return '$prefix${(absVal / 1e9).toStringAsFixed(2)} Mrd.'; } else if (absVal >= 1e6) { return '$prefix${(absVal / 1e6).toStringAsFixed(2)} Mio.'; } else if (absVal >= 1e3) { return '$prefix${(absVal / 1e3).toStringAsFixed(1)} Tsd.'; } else { return '$prefix${absVal.toStringAsFixed(2)}'; } } /// Leitet das Währungssymbol vom Ticker-Suffix ab. String _getCurrencySymbol(String? ticker) { if (ticker == null || ticker.isEmpty) return '\$'; final t = ticker.toUpperCase(); if (t.endsWith('.DE') || t.endsWith('.F') || t.endsWith('.STU') || t.endsWith('.MU') || t.endsWith('.HM') || t.endsWith('.DU') || t.endsWith('.BE') || t.endsWith('.SG') || t.endsWith('.PA') || t.endsWith('.AS') || t.endsWith('.MI') || t.endsWith('.MC')) return '€'; if (t.endsWith('.L')) return '£'; if (t.endsWith('.SW')) return 'CHF '; if (t.endsWith('.TO')) return 'CA\$'; if (t.endsWith('.AX')) return 'A\$'; if (t.endsWith('.T')) return '¥'; if (t.endsWith('.HK')) return 'HK\$'; return '\$'; } /// Leitet den Währungscode vom Ticker-Suffix ab. String _getCurrencyCode(String? ticker) { if (ticker == null || ticker.isEmpty) return 'USD'; final t = ticker.toUpperCase(); if (t.endsWith('.DE') || t.endsWith('.F') || t.endsWith('.STU') || t.endsWith('.MU') || t.endsWith('.HM') || t.endsWith('.DU') || t.endsWith('.BE') || t.endsWith('.SG') || t.endsWith('.PA') || t.endsWith('.AS') || t.endsWith('.MI') || t.endsWith('.MC')) return 'EUR'; if (t.endsWith('.L')) return 'GBP'; if (t.endsWith('.SW')) return 'CHF'; if (t.endsWith('.TO')) return 'CAD'; if (t.endsWith('.AX')) return 'AUD'; if (t.endsWith('.T')) return 'JPY'; if (t.endsWith('.HK')) return 'HKD'; return 'USD'; } } class _MetricRowItem { final String label; final String value; final Color? valueColor; const _MetricRowItem(this.label, this.value, {this.valueColor}); }