628 lines
26 KiB
Dart
628 lines
26 KiB
Dart
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<FundamentalsTab> createState() => _FundamentalsTabState();
|
|
}
|
|
|
|
class _FundamentalsTabState extends State<FundamentalsTab> {
|
|
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<AssetFundamentalsBloc, AssetFundamentalsState>(
|
|
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<AssetFundamentalsBloc>().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<String>(
|
|
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<TableRow> _buildTableRows(List<FinancialStatementModel> statements) {
|
|
final List<TableRow> 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<dynamic> 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<AssetFundamentalsBloc>().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)}';
|
|
}
|
|
}
|
|
}
|