import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../models/fundamental_data_model.dart'; import '../../../../core/theme/app_theme.dart'; import '../../../../core/widgets/glass_container.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 '../../utils/metric_explanations.dart'; class FundamentalsTab extends StatefulWidget { final String isin; final String? symbol; const FundamentalsTab({super.key, this.symbol, required this.isin}); @override State createState() => _FundamentalsTabState(); } class _FundamentalsTabState extends State { String _selectedPeriodType = 'Annual'; // 'Annual' or 'Quarterly' String _selectedStatementType = 'Income'; // 'Income', 'Balance', 'CashFlow' @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 Center(child: CircularProgressIndicator(color: AppTheme.primaryEmerald)); } 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) { return _buildEmptyState(); } return SingleChildScrollView( 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. Valuation Multiples & Ratios _buildSectionHeader('Bewertungskennzahlen & Multiples', Icons.analytics_outlined), const SizedBox(height: 12), GridView.count( crossAxisCount: MediaQuery.of(context).size.width > 700 ? 4 : 2, crossAxisSpacing: 10, mainAxisSpacing: 10, childAspectRatio: MediaQuery.of(context).size.width > 700 ? 2.2 : 1.8, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), children: [ _buildMetricCard('KGV (Trailing P/E)', _fmtMultiple(data.peRatioTrailing)), _buildMetricCard('KGV (Forward P/E)', _fmtMultiple(data.peRatioForward)), _buildMetricCard('PEG Ratio', _fmtMultiple(data.pegRatio)), _buildMetricCard('KBV (P/B Ratio)', _fmtMultiple(data.pbRatio)), _buildMetricCard('KUV (P/S Ratio)', _fmtMultiple(data.psRatio)), _buildMetricCard('EV / EBITDA', _fmtMultiple(data.evToEbitda)), _buildMetricCard('EV / Sales', _fmtMultiple(data.evToRevenue)), _buildMetricCard('Enterprise Value', _formatNumber(data.enterpriseValue)), _buildMetricCard('Marktkapitalisierung', _formatNumber(data.marketCapitalization)), _buildMetricCard('52W Höchststand', _fmtCurrency(data.fiftyTwoWeekHigh)), _buildMetricCard('52W Tiefststand', _fmtCurrency(data.fiftyTwoWeekLow)), _buildMetricCard('Short Ratio', _fmtMultiple(data.shortRatio)), ], ), const SizedBox(height: 24), // 3. Profitability & Financial Health Margins _buildSectionHeader('Rentabilität & Finanzielle Gesundheit', Icons.account_balance_outlined), const SizedBox(height: 12), GridView.count( crossAxisCount: MediaQuery.of(context).size.width > 700 ? 4 : 2, crossAxisSpacing: 10, mainAxisSpacing: 10, childAspectRatio: MediaQuery.of(context).size.width > 700 ? 2.2 : 1.8, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), children: [ _buildMetricCard('Bruttomarge (Gross)', _fmtPercent(data.grossMargin)), _buildMetricCard('Operative Marge', _fmtPercent(data.operatingMargin)), _buildMetricCard('Nettogewinnmarge', _fmtPercent(data.netProfitMargin)), _buildMetricCard('Eigenkapitalrendite (ROE)', _fmtPercent(data.returnOnEquity)), _buildMetricCard('Gesamtkapitalrendite (ROA)', _fmtPercent(data.returnOnAssets)), _buildMetricCard('ROIC (Invested Capital)', _fmtPercent(data.returnOnInvestedCapital)), _buildMetricCard('Verschuldungsgrad (D/E)', _fmtMultiple(data.debtToEquity)), _buildMetricCard('Current Ratio', _fmtMultiple(data.currentRatio)), _buildMetricCard('Quick Ratio', _fmtMultiple(data.quickRatio)), _buildMetricCard('Zinsdeckungsgrad', _fmtMultiple(data.interestCoverage)), ], ), const SizedBox(height: 24), // 4. Dividends & Ownership _buildSectionHeader('Dividenden & Aktionärsstruktur', Icons.pie_chart_outline), const SizedBox(height: 12), GridView.count( crossAxisCount: MediaQuery.of(context).size.width > 700 ? 4 : 2, crossAxisSpacing: 10, mainAxisSpacing: 10, childAspectRatio: MediaQuery.of(context).size.width > 700 ? 2.2 : 1.8, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), children: [ _buildMetricCard('Dividendenrendite', _fmtPercent(data.dividendYield)), _buildMetricCard('Ausschüttungsquote (Payout)', _fmtPercent(data.payoutRatio)), _buildMetricCard('Ex-Dividendentag', _fmtDate(data.exDividendDate)), _buildMetricCard('Nächste Quartalszahlen', _fmtDate(data.nextEarningsDate)), _buildMetricCard('Institutioneller Anteil', _fmtPercent(data.percentHeldByInstitutions)), _buildMetricCard('Insider Anteil', _fmtPercent(data.percentHeldByInsiders)), _buildMetricCard('Short % of Float', _fmtPercent(data.shortPercentOfFloat)), ], ), const SizedBox(height: 24), // 5. Financial Statements Section _buildSectionHeader('Finanzberichte (Statements)', Icons.article_outlined), const SizedBox(height: 12), _buildStatementsSection(data), const SizedBox(height: 24), // 6. 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 _buildStatementsSection(FundamentalDataModel data) { // Filter statements by Jährlich / Quartal final filteredStatements = data.financialStatements .where((s) => s.periodType.toLowerCase() == _selectedPeriodType.toLowerCase()) .toList(); // Sort descending by date filteredStatements.sort((a, b) => b.endDate.compareTo(a.endDate)); return GlassContainer( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Row containing switches Row( children: [ // Period Toggle (Annual / Quarterly) DropdownButton( value: _selectedPeriodType, dropdownColor: AppTheme.cardSurface, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), underline: const SizedBox.shrink(), icon: const Icon(Icons.arrow_drop_down, color: Colors.white), items: const [ DropdownMenuItem(value: 'Annual', child: Text('Jährlich (Annual)')), DropdownMenuItem(value: 'Quarterly', child: Text('Quartal (Quarterly)')), ], onChanged: (val) { if (val != null) { setState(() => _selectedPeriodType = val); } }, ), const Spacer(), // Statement Type Selector Row( children: [ _buildStatementTabButton('GuV', 'Income'), const SizedBox(width: 6), _buildStatementTabButton('Bilanz', 'Balance'), const SizedBox(width: 6), _buildStatementTabButton('Cashflow', 'CashFlow'), ], ), ], ), const SizedBox(height: 16), const Divider(color: Colors.white10), const SizedBox(height: 8), if (filteredStatements.isEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 24), child: Center( child: Text( 'Keine Berichte für diesen Typ vorhanden.', style: TextStyle(color: AppTheme.textMuted, fontStyle: FontStyle.italic), ), ), ) else SingleChildScrollView( scrollDirection: Axis.horizontal, physics: const BouncingScrollPhysics(), child: Table( defaultColumnWidth: const FixedColumnWidth(110), columnWidths: const { 0: FixedColumnWidth(180), // First column containing label is wider }, border: TableBorder( horizontalInside: BorderSide(color: Colors.white, width: 0.5), ), children: _buildTableRows(filteredStatements), ), ), ], ), ); } Widget _buildStatementTabButton(String label, String typeCode) { final isSelected = _selectedStatementType == typeCode; return InkWell( onTap: () => setState(() => _selectedStatementType = typeCode), borderRadius: BorderRadius.circular(8), child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: isSelected ? AppTheme.primaryEmerald.withValues(alpha: 0.15) : Colors.transparent, borderRadius: BorderRadius.circular(8), border: Border.all( color: isSelected ? AppTheme.primaryEmerald.withValues(alpha: 0.4) : Colors.white10, ), ), child: Text( label, style: TextStyle( color: isSelected ? AppTheme.primaryEmerald : Colors.white70, fontSize: 12, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, ), ), ), ); } List _buildTableRows(List statements) { final List rows = []; // Header row containing Dates rows.add( TableRow( children: [ _buildTableCell('Kennzahl (in EUR)', isHeader: true), ...statements.map((s) => _buildTableCell(_fmtDate(s.endDate), isHeader: true)), ], ), ); if (_selectedStatementType == 'Income') { rows.add(_buildDataRow('Umsatzerlöse', statements.map((s) => s.totalRevenue).toList())); rows.add(_buildDataRow('Umsatzkosten', statements.map((s) => s.costOfRevenue).toList())); rows.add(_buildDataRow('Bruttogewinn', statements.map((s) => s.grossProfit).toList())); rows.add(_buildDataRow('Operative Aufwendungen', statements.map((s) => s.operatingExpenses).toList())); rows.add(_buildDataRow('Operatives Ergebnis (EBIT)', statements.map((s) => s.operatingIncome).toList())); rows.add(_buildDataRow('EBITDA', statements.map((s) => s.ebitda).toList())); rows.add(_buildDataRow('Jahresüberschuss', statements.map((s) => s.netIncome).toList())); rows.add(_buildDataRow('EPS (Basic)', statements.map((s) => s.epsBasic).toList(), isCurrency: true)); rows.add(_buildDataRow('EPS (Diluted)', statements.map((s) => s.epsDiluted).toList(), isCurrency: true)); } else if (_selectedStatementType == 'Balance') { rows.add(_buildDataRow('Liquide Mittel', statements.map((s) => s.cashAndCashEquivalents).toList())); rows.add(_buildDataRow('Forderungen', statements.map((s) => s.accountsReceivable).toList())); rows.add(_buildDataRow('Vorräte', statements.map((s) => s.inventory).toList())); rows.add(_buildDataRow('Umlaufvermögen (Current Assets)', statements.map((s) => s.totalCurrentAssets).toList())); rows.add(_buildDataRow('Anlagevermögen (Non-Current)', statements.map((s) => s.totalNonCurrentAssets).toList())); rows.add(_buildDataRow('Kurzfr. Verbindlichkeiten', statements.map((s) => s.currentLiabilities).toList())); rows.add(_buildDataRow('Langfristige Schulden', statements.map((s) => s.longTermDebt).toList())); rows.add(_buildDataRow('Gesamtverbindlichkeiten', statements.map((s) => s.totalLiabilities).toList())); rows.add(_buildDataRow('Eigenkapital (Equity)', statements.map((s) => s.totalStockholdersEquity).toList())); } else { rows.add(_buildDataRow('Operativer Cashflow', statements.map((s) => s.operatingCashFlow).toList())); rows.add(_buildDataRow('Investiver Cashflow', statements.map((s) => s.investingCashFlow).toList())); rows.add(_buildDataRow('Investitionsausgaben (CapEx)', statements.map((s) => s.capitalExpenditures).toList())); rows.add(_buildDataRow('Finanzierungs-Cashflow', statements.map((s) => s.financingCashFlow).toList())); rows.add(_buildDataRow('Free Cashflow', statements.map((s) => s.freeCashFlow).toList())); } return rows; } TableRow _buildDataRow(String label, List values, {bool isCurrency = false}) { return TableRow( children: [ _buildTableCell(label), ...values.map((v) => _buildTableCell(isCurrency ? _fmtCurrency(v) : _formatNumber(v))), ], ); } Widget _buildTableCell(String val, {bool isHeader = false}) { return Padding( padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8), child: Text( val, style: TextStyle( color: isHeader ? AppTheme.accentCyan : Colors.white70, fontWeight: isHeader ? FontWeight.bold : FontWeight.normal, fontSize: 12, ), ), ); } 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 _buildMetricCard(String label, String value) { return InkWell( onTap: () => MetricExplanations.show(context, label), borderRadius: BorderRadius.circular(10), child: GlassContainer( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( label, style: TextStyle(color: AppTheme.textMuted, fontSize: 11), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 4), Icon(Icons.info_outline, size: 12, color: AppTheme.textMuted), ], ), const SizedBox(height: 4), Expanded( child: Align( alignment: Alignment.centerLeft, child: FittedBox( fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text( value, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14), ), ), ), ), ], ), ), ); } 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 _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'; final p = (n > 0 && n <= 1) ? 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()); return n != null ? '€${n.toStringAsFixed(2)}' : 'N/A'; } 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 ? '-€' : '€'; if (absVal >= 1e12) { return '$prefix${(absVal / 1e12).toStringAsFixed(2)} Bil.'; } 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(2)} Tsd.'; } else { return '$prefix${absVal.toStringAsFixed(2)}'; } } }