feat(asset_detail): modular fundamentals sections, executive salaries and logo resolution

This commit is contained in:
2026-08-15 01:03:30 +02:00
parent 15f8f7896e
commit 1ccb6b613f
21 changed files with 2011 additions and 2081 deletions
@@ -4,9 +4,6 @@ import '../../../../core/theme/app_theme.dart';
import '../../../favorites/cubit/favorites_cubit.dart';
import '../../bloc/fundamentals/asset_fundamentals_bloc.dart';
import '../../bloc/fundamentals/asset_fundamentals_event.dart';
import '../../bloc/header/asset_header_bloc.dart';
import '../../bloc/header/asset_header_event.dart';
import '../../bloc/header/asset_header_state.dart';
import '../../bloc/technical/asset_technical_bloc.dart';
import '../../bloc/technical/asset_technical_event.dart';
import '../../bloc/trades/asset_trades_bloc.dart';
@@ -21,8 +18,12 @@ class AssetPageDesktopLayout extends StatefulWidget {
final String? name;
final String? selectedTicker;
const AssetPageDesktopLayout(
{super.key, required this.isin, this.selectedTicker, this.name});
const AssetPageDesktopLayout({
super.key,
required this.isin,
this.selectedTicker,
this.name,
});
@override
State<AssetPageDesktopLayout> createState() => _AssetPageDesktopLayoutState();
@@ -31,12 +32,12 @@ class AssetPageDesktopLayout extends StatefulWidget {
class _AssetPageDesktopLayoutState extends State<AssetPageDesktopLayout>
with SingleTickerProviderStateMixin {
late TabController _tabController;
String? _selectedExchange;
String? _selectedTicker;
@override
void initState() {
super.initState();
_selectedTicker = widget.selectedTicker;
_tabController = TabController(length: 3, vsync: this);
}
@@ -48,11 +49,8 @@ class _AssetPageDesktopLayoutState extends State<AssetPageDesktopLayout>
void _handleExchangeChanged(String newExchange, String newTicker) {
setState(() {
_selectedExchange = newExchange;
_selectedTicker = newTicker;
});
context.read<AssetHeaderBloc>().add(
LoadAssetHeader(widget.isin, exchange: newExchange, ticker: newTicker));
context.read<AssetFundamentalsBloc>().add(LoadAssetFundamentals(widget.isin,
ticker: newTicker, forceRefresh: false));
context.read<AssetTechnicalBloc>().add(LoadAssetTechnical(widget.isin,
@@ -65,10 +63,8 @@ class _AssetPageDesktopLayoutState extends State<AssetPageDesktopLayout>
}
void _handleForceRefresh() {
context.read<AssetHeaderBloc>().add(LoadAssetHeader(widget.isin,
forceRefresh: true,
exchange: _selectedExchange,
ticker: _selectedTicker));
context.read<AssetFundamentalsBloc>().add(LoadAssetFundamentals(widget.isin,
ticker: _selectedTicker, forceRefresh: true));
context.read<AssetTechnicalBloc>().add(LoadAssetTechnical(widget.isin,
ticker: _selectedTicker, forceRefresh: true));
context.read<AssetTradesBloc>().add(LoadAssetTrades(widget.isin));
@@ -78,119 +74,100 @@ class _AssetPageDesktopLayoutState extends State<AssetPageDesktopLayout>
Widget build(BuildContext context) {
final theme = AppTheme.activePreset;
return BlocListener<AssetHeaderBloc, AssetHeaderState>(
listener: (context, state) {
if (state is AssetHeaderLoaded && state.data != null) {
if (_selectedTicker == null) {
setState(() {
_selectedTicker = widget.selectedTicker;
});
}
context.read<AssetFundamentalsBloc>().add(LoadAssetFundamentals(
widget.isin,
ticker: _selectedTicker,
forceRefresh: false));
context.read<AssetTechnicalBloc>().add(LoadAssetTechnical(
widget.isin,
ticker: _selectedTicker,
forceRefresh: false));
}
},
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 1. Hero Header
AssetHeroHeader(
return SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 1. Hero Header
AssetHeroHeader(
isin: widget.isin,
name: widget.name ?? widget.isin,
symbol: _selectedTicker ?? widget.selectedTicker,
onExchangeChanged: _handleExchangeChanged,
onForceRefresh: _handleForceRefresh,
),
// 2. Full-Width Interactive Chart Section
Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: theme.cardSurface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: theme.glassBorder),
),
child: TechnicalTab(
isin: widget.isin,
name: widget.name ?? widget.isin,
symbol: _selectedTicker ?? widget.selectedTicker,
onExchangeChanged: _handleExchangeChanged,
onForceRefresh: _handleForceRefresh,
symbol: _selectedTicker,
showChartOnly: true,
chartHeight: 460,
),
),
// 2. Full-Width Interactive Chart Section
Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: theme.cardSurface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: theme.glassBorder),
),
child: TechnicalTab(
isin: widget.isin,
symbol: _selectedTicker,
showChartOnly: true,
chartHeight: 460,
),
const SizedBox(height: 8),
// 3. Detailed Sections & Fundamentals under the Chart
Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: theme.cardSurface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: theme.glassBorder),
),
const SizedBox(height: 8),
// 3. Detailed Sections & Fundamentals under the Chart
Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: theme.cardSurface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: theme.glassBorder),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TabBar(
controller: _tabController,
labelColor: theme.primaryColor,
unselectedLabelColor: theme.textMuted,
indicatorColor: theme.primaryColor,
dividerColor: theme.glassBorder,
labelStyle: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 13),
tabs: const [
Tab(
icon: Icon(Icons.analytics_outlined, size: 18),
text: 'FUNDAMENTALS & ÜBERSICHT'),
Tab(
icon: Icon(Icons.architecture_outlined, size: 18),
text: 'MUSTER & SIGNALE'),
Tab(
icon: Icon(Icons.candlestick_chart_outlined, size: 18),
text: 'TRADES'),
],
),
AnimatedBuilder(
animation: _tabController,
builder: (context, _) {
switch (_tabController.index) {
case 0:
return FundamentalsTab(
isin: widget.isin,
symbol: _selectedTicker,
isEmbedded: true,
);
case 1:
return TechnicalTab(
isin: widget.isin,
symbol: _selectedTicker,
showDetailsOnly: true,
);
case 2:
return SizedBox(
height: 600,
child: TradesTab(symbol: widget.isin),
);
default:
return const SizedBox.shrink();
}
},
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TabBar(
controller: _tabController,
labelColor: theme.primaryColor,
unselectedLabelColor: theme.textMuted,
indicatorColor: theme.primaryColor,
dividerColor: theme.glassBorder,
labelStyle: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 13),
tabs: const [
Tab(
icon: Icon(Icons.analytics_outlined, size: 18),
text: 'FUNDAMENTALS & ÜBERSICHT'),
Tab(
icon: Icon(Icons.architecture_outlined, size: 18),
text: 'MUSTER & SIGNALE'),
Tab(
icon: Icon(Icons.candlestick_chart_outlined, size: 18),
text: 'TRADES'),
],
),
AnimatedBuilder(
animation: _tabController,
builder: (context, _) {
switch (_tabController.index) {
case 0:
return FundamentalsTab(
isin: widget.isin,
symbol: _selectedTicker,
isEmbedded: true,
);
case 1:
return TechnicalTab(
isin: widget.isin,
symbol: _selectedTicker,
showDetailsOnly: true,
);
case 2:
return SizedBox(
height: 600,
child: TradesTab(symbol: widget.isin),
);
default:
return const SizedBox.shrink();
}
},
),
],
),
const SizedBox(height: 24),
],
),
),
const SizedBox(height: 24),
],
),
);
}
@@ -4,9 +4,6 @@ import '../../../../core/theme/app_theme.dart';
import '../../../favorites/cubit/favorites_cubit.dart';
import '../../bloc/fundamentals/asset_fundamentals_bloc.dart';
import '../../bloc/fundamentals/asset_fundamentals_event.dart';
import '../../bloc/header/asset_header_bloc.dart';
import '../../bloc/header/asset_header_event.dart';
import '../../bloc/header/asset_header_state.dart';
import '../../bloc/technical/asset_technical_bloc.dart';
import '../../bloc/technical/asset_technical_event.dart';
import '../../bloc/trades/asset_trades_bloc.dart';
@@ -21,8 +18,12 @@ class AssetPageMobileLayout extends StatefulWidget {
final String? name;
final String? selectedTicker;
const AssetPageMobileLayout(
{super.key, required this.isin, this.selectedTicker, this.name});
const AssetPageMobileLayout({
super.key,
required this.isin,
this.selectedTicker,
this.name,
});
@override
State<AssetPageMobileLayout> createState() => _AssetPageMobileLayoutState();
@@ -36,7 +37,7 @@ class _AssetPageMobileLayoutState extends State<AssetPageMobileLayout>
@override
void initState() {
super.initState();
//_selectedTicker = widget.selectedTicker;
_selectedTicker = widget.selectedTicker;
_tabController = TabController(length: 3, vsync: this);
}
@@ -48,11 +49,8 @@ class _AssetPageMobileLayoutState extends State<AssetPageMobileLayout>
void _handleExchangeChanged(String newExchange, String newTicker) {
setState(() {
//_selectedExchange = newExchange;
_selectedTicker = newTicker;
});
context.read<AssetHeaderBloc>().add(
LoadAssetHeader(widget.isin, exchange: newExchange, ticker: newTicker));
context.read<AssetFundamentalsBloc>().add(LoadAssetFundamentals(widget.isin,
ticker: newTicker, forceRefresh: false));
context.read<AssetTechnicalBloc>().add(LoadAssetTechnical(widget.isin,
@@ -65,10 +63,8 @@ class _AssetPageMobileLayoutState extends State<AssetPageMobileLayout>
}
void _handleForceRefresh() {
context.read<AssetHeaderBloc>().add(LoadAssetHeader(widget.isin,
forceRefresh: true, ticker: _selectedTicker));
// AssetFundamentalsBloc is omitted here because AssetHeaderBloc already triggers forceRefresh=true
// for fundamentals, and the listener below will fetch the updated data with forceRefresh=false.
context.read<AssetFundamentalsBloc>().add(LoadAssetFundamentals(widget.isin,
ticker: _selectedTicker, forceRefresh: true));
context.read<AssetTechnicalBloc>().add(LoadAssetTechnical(widget.isin,
ticker: _selectedTicker, forceRefresh: true));
context.read<AssetTradesBloc>().add(LoadAssetTrades(widget.isin));
@@ -78,113 +74,102 @@ class _AssetPageMobileLayoutState extends State<AssetPageMobileLayout>
Widget build(BuildContext context) {
final theme = AppTheme.activePreset;
return BlocListener<AssetHeaderBloc, AssetHeaderState>(
listener: (context, state) {
if (state is AssetHeaderLoaded && state.data != null) {
if (_selectedTicker == null) {
setState(() {
_selectedTicker = widget.selectedTicker;
});
}
context.read<AssetFundamentalsBloc>().add(LoadAssetFundamentals(
widget.isin,
ticker: _selectedTicker,
forceRefresh: false));
context.read<AssetTechnicalBloc>().add(LoadAssetTechnical(
widget.isin,
ticker: _selectedTicker,
forceRefresh: false));
}
},
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 1. Hero Header
AssetHeroHeader(
return SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 1. Hero Header
AssetHeroHeader(
isin: widget.isin,
name: widget.name ?? widget.isin,
symbol: _selectedTicker ?? widget.selectedTicker,
onExchangeChanged: _handleExchangeChanged,
onForceRefresh: _handleForceRefresh,
),
// 2. Interactive Chart
Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: theme.cardSurface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: theme.glassBorder),
),
child: TechnicalTab(
isin: widget.isin,
name: widget.name ?? widget.isin,
symbol: _selectedTicker ?? widget.selectedTicker,
onExchangeChanged: _handleExchangeChanged,
onForceRefresh: _handleForceRefresh,
symbol: _selectedTicker,
showChartOnly: true,
chartHeight: 320,
),
),
// 2. Full-Width Interactive Chart Section
Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: theme.cardSurface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: theme.glassBorder),
),
child: TechnicalTab(
isin: widget.isin,
symbol: _selectedTicker,
showChartOnly: true,
chartHeight: 330,
),
const SizedBox(height: 8),
// 3. Tabbed Detailed Analysis
Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: theme.cardSurface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: theme.glassBorder),
),
const SizedBox(height: 6),
// 3. Tab Bar & Detailed Sections (Fundamentals, Signals, Trades)
Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: theme.cardSurface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: theme.glassBorder),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TabBar(
controller: _tabController,
labelColor: theme.primaryColor,
unselectedLabelColor: theme.textMuted,
indicatorColor: theme.primaryColor,
dividerColor: theme.glassBorder,
labelStyle: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 12),
tabs: const [
Tab(text: 'FUNDAMENTALS'),
Tab(text: 'MUSTER & SIGNALE'),
Tab(text: 'TRADES'),
],
),
AnimatedBuilder(
animation: _tabController,
builder: (context, _) {
switch (_tabController.index) {
case 0:
return FundamentalsTab(
isin: widget.isin,
symbol: _selectedTicker,
isEmbedded: true,
);
case 1:
return TechnicalTab(
isin: widget.isin,
symbol: _selectedTicker,
showDetailsOnly: true,
);
case 2:
return SizedBox(
height: 500,
child: TradesTab(symbol: widget.isin),
);
default:
return const SizedBox.shrink();
}
},
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TabBar(
controller: _tabController,
labelColor: theme.primaryColor,
unselectedLabelColor: theme.textMuted,
indicatorColor: theme.primaryColor,
dividerColor: theme.glassBorder,
isScrollable: true,
tabAlignment: TabAlignment.start,
labelStyle: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 12),
tabs: const [
Tab(
icon: Icon(Icons.analytics_outlined, size: 16),
text: 'FUNDAMENTALS'),
Tab(
icon: Icon(Icons.architecture_outlined, size: 16),
text: 'MUSTER & SIGNALE'),
Tab(
icon: Icon(Icons.candlestick_chart_outlined, size: 16),
text: 'TRADES'),
],
),
AnimatedBuilder(
animation: _tabController,
builder: (context, _) {
switch (_tabController.index) {
case 0:
return FundamentalsTab(
isin: widget.isin,
symbol: _selectedTicker,
isEmbedded: true,
);
case 1:
return TechnicalTab(
isin: widget.isin,
symbol: _selectedTicker,
showDetailsOnly: true,
);
case 2:
return SizedBox(
height: 500,
child: TradesTab(symbol: widget.isin),
);
default:
return const SizedBox.shrink();
}
},
),
],
),
const SizedBox(height: 24),
],
),
),
const SizedBox(height: 24),
],
),
);
}