226 lines
8.3 KiB
Dart
226 lines
8.3 KiB
Dart
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 '../../bloc/fundamentals/asset_fundamentals_bloc.dart';
|
|
import '../../bloc/fundamentals/asset_fundamentals_event.dart';
|
|
import '../../bloc/fundamentals/asset_fundamentals_state.dart';
|
|
import '../../widgets/fundamentals/analyst_price_target_card.dart';
|
|
import '../../widgets/fundamentals/fundamental_category_panels.dart';
|
|
import '../../widgets/fundamentals/company_profile_section.dart';
|
|
|
|
class FundamentalsTab extends StatelessWidget {
|
|
final String isin;
|
|
final String? symbol;
|
|
final bool isEmbedded;
|
|
|
|
const FundamentalsTab({
|
|
super.key,
|
|
this.symbol,
|
|
this.isEmbedded = false,
|
|
required this.isin,
|
|
});
|
|
|
|
String _getCurrencySymbol(String? ticker) {
|
|
if (ticker == null || ticker.isEmpty) return '€';
|
|
final t = ticker.toUpperCase();
|
|
if (t.endsWith('.DE') || t.endsWith('.F') || t.endsWith('.VI') || t.endsWith('.PA') || t.endsWith('.AS') || t.endsWith('.MC') || t.endsWith('.MI')) {
|
|
return '€';
|
|
}
|
|
if (t.endsWith('.L')) return '£';
|
|
if (t.endsWith('.TO') || t.endsWith('.V')) return 'CA\$';
|
|
return '\$';
|
|
}
|
|
|
|
String _getCurrencyCode(String? ticker) {
|
|
if (ticker == null || ticker.isEmpty) return 'EUR';
|
|
final t = ticker.toUpperCase();
|
|
if (t.endsWith('.DE') || t.endsWith('.F') || t.endsWith('.VI') || t.endsWith('.PA') || t.endsWith('.AS') || t.endsWith('.MC') || t.endsWith('.MI')) {
|
|
return 'EUR';
|
|
}
|
|
if (t.endsWith('.L')) return 'GBP';
|
|
if (t.endsWith('.TO') || t.endsWith('.V')) return 'CAD';
|
|
return 'USD';
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return BlocBuilder<AssetFundamentalsBloc, AssetFundamentalsState>(
|
|
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<AssetFundamentalsBloc>().add(LoadAssetFundamentals(isin, ticker: symbol, forceRefresh: true)),
|
|
icon: const Icon(Icons.refresh),
|
|
label: const Text('Erneut versuchen'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
if (state is AssetFundamentalsLoaded && state.data != null) {
|
|
final data = state.data!;
|
|
final sym = _getCurrencySymbol(data.ticker);
|
|
final curCode = _getCurrencyCode(data.ticker);
|
|
|
|
return SingleChildScrollView(
|
|
physics: isEmbedded ? const NeverScrollableScrollPhysics() : null,
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 1. Analyst Forecasts & Price Targets Header Card
|
|
AnalystPriceTargetCard(data: data, currencySymbol: sym),
|
|
const SizedBox(height: 20),
|
|
|
|
// 2. Responsive Side-by-Side Category List Panels (Valuation, Profitability, Dividends)
|
|
FundamentalCategoryPanels(data: data, currencySymbol: sym, currencyCode: curCode),
|
|
const SizedBox(height: 20),
|
|
|
|
// 3. Company Description & Detailed Executive Board
|
|
_buildSectionHeader('Unternehmensprofil & Führungskräfte', Icons.business_outlined),
|
|
const SizedBox(height: 12),
|
|
CompanyProfileSection(data: data, currencySymbol: sym),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
return _buildEmptyState(context);
|
|
},
|
|
);
|
|
}
|
|
|
|
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 _buildEmptyState(BuildContext context) {
|
|
return Center(
|
|
child: GlassContainer(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(Icons.analytics_outlined, color: AppTheme.textMuted, size: 48),
|
|
const SizedBox(height: 12),
|
|
const Text('Keine Fundamentaldaten verfügbar.', style: TextStyle(color: Colors.white70)),
|
|
const SizedBox(height: 16),
|
|
ElevatedButton.icon(
|
|
onPressed: () => context.read<AssetFundamentalsBloc>().add(LoadAssetFundamentals(isin, ticker: symbol, forceRefresh: true)),
|
|
icon: const Icon(Icons.refresh),
|
|
label: const Text('Aktualisieren'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
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: isEmbedded ? const NeverScrollableScrollPhysics() : null,
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const ShimmerLoading(width: double.infinity, height: 86, borderRadius: 16),
|
|
const SizedBox(height: 20),
|
|
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),
|
|
const ShimmerLoading(width: 220, height: 20, borderRadius: 6),
|
|
const SizedBox(height: 12),
|
|
const ShimmerLoading(width: double.infinity, height: 140, borderRadius: 16),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|