feat(fundamentals): preserve ticker selection and add deterministic key executives sorting

This commit is contained in:
2026-08-15 19:30:04 +02:00
parent 960a4bdbd6
commit 7e18257e3e
11 changed files with 942 additions and 235 deletions
@@ -33,6 +33,7 @@ class AssetHeroHeader extends StatelessWidget {
return BlocBuilder<AssetFundamentalsBloc, AssetFundamentalsState>(
builder: (context, fundState) {
String displayName = name;
String primaryTicker = '';
final String? logoUrl = isin.isNotEmpty ? '/api/v1/logo/$isin' : null;
List<TickerModel> tickerOptions = [
TickerModel(ticker: symbol ?? 'Loading', exchange: 'Loading', tradingCurrency: '', currentPrice: null)
@@ -43,14 +44,21 @@ class AssetHeroHeader extends StatelessWidget {
if (data.companyName.isNotEmpty) {
displayName = data.companyName;
}
if (data.primaryTicker.isNotEmpty) {
primaryTicker = data.primaryTicker;
}
if (data.availableTickers.isNotEmpty) {
tickerOptions = data.availableTickers;
}
}
final selectedOption = tickerOptions.firstWhere(
(t) => t.ticker == symbol || t.exchange == symbol,
orElse: () => tickerOptions.first,
(t) => (symbol != null && symbol!.isNotEmpty) &&
(t.ticker.toLowerCase() == symbol!.toLowerCase() || (t.exchange != null && t.exchange!.toLowerCase() == symbol!.toLowerCase())),
orElse: () => tickerOptions.firstWhere(
(t) => primaryTicker.isNotEmpty && t.ticker.toLowerCase() == primaryTicker.toLowerCase(),
orElse: () => tickerOptions.first,
),
);
return Container(
@@ -203,73 +211,125 @@ class AssetHeroHeader extends StatelessWidget {
);
},
),
// Interactive Ticker & Exchange Selector Dropdown
PopupMenuButton<String>(
initialValue: selectedOption.ticker,
tooltip: 'Select Exchange & Ticker',
onSelected: (newTicker) {
if (onExchangeChanged != null) {
final opt = tickerOptions.firstWhere(
(t) => t.ticker == newTicker,
orElse: () => tickerOptions.first,
);
onExchangeChanged!(opt.exchange ?? 'Unknown', opt.ticker);
}
},
itemBuilder: (context) {
return tickerOptions.map((opt) {
final ex = opt.exchange ?? 'Unknown';
final tick = opt.ticker;
final label = '$tick ($ex)';
final isSelected = tick == symbol || ex == symbol;
PopupMenuButton<String>(
initialValue: selectedOption.ticker,
tooltip: 'Börsenplatz & Ticker auswählen',
color: theme.cardSurface,
onSelected: (newTicker) {
if (onExchangeChanged != null) {
final opt = tickerOptions.firstWhere(
(t) => t.ticker == newTicker,
orElse: () => tickerOptions.first,
);
onExchangeChanged!(opt.exchange ?? 'Unknown', opt.ticker);
}
},
itemBuilder: (context) {
return tickerOptions.map((opt) {
final ex = opt.exchange ?? 'Unknown';
final tick = opt.ticker;
final isPrimary = primaryTicker.isNotEmpty &&
(tick.toLowerCase() == primaryTicker.toLowerCase());
final isSelected = tick == symbol || ex == symbol;
return PopupMenuItem<String>(
value: tick,
child: Row(
children: [
Icon(
Icons.business,
size: 16,
color: isSelected ? theme.primaryColor : theme.textMuted,
return PopupMenuItem<String>(
value: tick,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 2),
child: Row(
children: [
Icon(
isPrimary ? Icons.star_rounded : Icons.business,
size: 18,
color: isPrimary
? AppTheme.accentCyan
: (isSelected ? theme.primaryColor : theme.textMuted),
),
const SizedBox(width: 8),
Expanded(
child: Text(
'$tick ($ex)',
style: TextStyle(
fontWeight: (isSelected || isPrimary) ? FontWeight.bold : FontWeight.normal,
color: isSelected
? theme.primaryColor
: (isPrimary ? Colors.white : theme.textPrimary),
),
),
),
if (isPrimary) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: AppTheme.accentCyan.withValues(alpha: 0.18),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: AppTheme.accentCyan.withValues(alpha: 0.5)),
),
child: Text(
'PRIMARY',
style: TextStyle(
color: AppTheme.accentCyan,
fontSize: 9,
fontWeight: FontWeight.w900,
letterSpacing: 0.6,
),
),
),
],
],
),
const SizedBox(width: 8),
Text(
label,
style: TextStyle(
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected ? theme.primaryColor : theme.textPrimary,
),
),
],
),
);
}).toList();
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: theme.accentColor.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: theme.accentColor.withValues(alpha: 0.4)),
),
child: Row(
children: [
Icon(Icons.business, size: 14, color: theme.accentColor),
const SizedBox(width: 6),
Text(
'${selectedOption.ticker} (${selectedOption.exchange ?? 'Unknown'})',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: theme.accentColor,
),
);
}).toList();
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: (primaryTicker.isNotEmpty && selectedOption.ticker.toLowerCase() == primaryTicker.toLowerCase())
? AppTheme.accentCyan.withValues(alpha: 0.15)
: theme.accentColor.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: (primaryTicker.isNotEmpty && selectedOption.ticker.toLowerCase() == primaryTicker.toLowerCase())
? AppTheme.accentCyan.withValues(alpha: 0.5)
: theme.accentColor.withValues(alpha: 0.4),
),
const SizedBox(width: 4),
Icon(Icons.arrow_drop_down, size: 16, color: theme.accentColor),
],
),
child: Row(
children: [
Icon(
(primaryTicker.isNotEmpty && selectedOption.ticker.toLowerCase() == primaryTicker.toLowerCase())
? Icons.star_rounded
: Icons.business,
size: 15,
color: (primaryTicker.isNotEmpty && selectedOption.ticker.toLowerCase() == primaryTicker.toLowerCase())
? AppTheme.accentCyan
: theme.accentColor,
),
const SizedBox(width: 6),
Text(
'${selectedOption.ticker} (${selectedOption.exchange ?? 'Unknown'})',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: (primaryTicker.isNotEmpty && selectedOption.ticker.toLowerCase() == primaryTicker.toLowerCase())
? AppTheme.accentCyan
: theme.accentColor,
),
),
const SizedBox(width: 4),
Icon(
Icons.arrow_drop_down,
size: 16,
color: (primaryTicker.isNotEmpty && selectedOption.ticker.toLowerCase() == primaryTicker.toLowerCase())
? AppTheme.accentCyan
: theme.accentColor,
),
],
),
),
),
),
],
),
],