feat(app): responsive asset detail layout, full width chart, reactive hero header, shimmer loaders and enriched fundamentals
This commit is contained in:
@@ -29,6 +29,16 @@ class FundamentalDataModel extends Equatable {
|
||||
final double? evToEbitda;
|
||||
final double? evToRevenue;
|
||||
|
||||
final double? totalRevenue;
|
||||
final double? revenueGrowthYoY;
|
||||
final double? grossProfit;
|
||||
final double? ebitda;
|
||||
final double? dilutedEps;
|
||||
final double? totalCash;
|
||||
final double? totalDebt;
|
||||
final double? operatingCashFlow;
|
||||
final double? freeCashFlow;
|
||||
|
||||
final double? grossMargin;
|
||||
final double? operatingMargin;
|
||||
final double? netProfitMargin;
|
||||
@@ -86,6 +96,15 @@ class FundamentalDataModel extends Equatable {
|
||||
this.psRatio,
|
||||
this.evToEbitda,
|
||||
this.evToRevenue,
|
||||
this.totalRevenue,
|
||||
this.revenueGrowthYoY,
|
||||
this.grossProfit,
|
||||
this.ebitda,
|
||||
this.dilutedEps,
|
||||
this.totalCash,
|
||||
this.totalDebt,
|
||||
this.operatingCashFlow,
|
||||
this.freeCashFlow,
|
||||
this.grossMargin,
|
||||
this.operatingMargin,
|
||||
this.netProfitMargin,
|
||||
@@ -128,14 +147,121 @@ class FundamentalDataModel extends Equatable {
|
||||
return double.tryParse(val.toString());
|
||||
}
|
||||
|
||||
final assetMap = json['asset'] is Map<String, dynamic> ? json['asset'] as Map<String, dynamic> : null;
|
||||
final fundMap = json['fundamentals'] is Map<String, dynamic> ? json['fundamentals'] as Map<String, dynamic> : null;
|
||||
|
||||
String extractTickerStr(dynamic val) {
|
||||
if (val == null) return '';
|
||||
if (val is Map<String, dynamic>) {
|
||||
return val['ticker']?.toString() ?? '';
|
||||
}
|
||||
return val.toString();
|
||||
}
|
||||
|
||||
String? extractExchangeStr(dynamic val) {
|
||||
if (val == null) return null;
|
||||
if (val is Map<String, dynamic>) {
|
||||
return val['exchange']?.toString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
final isinVal = assetMap?['isin']?.toString() ?? json['isin']?.toString() ?? '';
|
||||
final primaryTickerVal = extractTickerStr(assetMap?['primaryTicker'] ?? json['primaryTicker']);
|
||||
final tickerVal = extractTickerStr(fundMap?['ticker'] ?? json['ticker']).isNotEmpty
|
||||
? extractTickerStr(fundMap?['ticker'] ?? json['ticker'])
|
||||
: primaryTickerVal;
|
||||
final companyNameVal = assetMap?['name']?.toString() ?? json['companyName']?.toString() ?? json['name']?.toString() ?? tickerVal;
|
||||
final businessSummaryVal = assetMap?['description']?.toString() ?? json['businessSummary']?.toString() ?? json['description']?.toString();
|
||||
|
||||
final exchangeVal = extractExchangeStr(fundMap?['ticker']) ??
|
||||
extractExchangeStr(assetMap?['primaryTicker']) ??
|
||||
json['exchange']?.toString();
|
||||
|
||||
final rawTickers = assetMap?['availableTickers'] ?? json['availableTickers'];
|
||||
List<TickerModel> availableTickersList = [];
|
||||
if (rawTickers is List) {
|
||||
availableTickersList = rawTickers.map((t) {
|
||||
if (t is Map<String, dynamic>) {
|
||||
return TickerModel.fromJson(t);
|
||||
} else {
|
||||
return TickerModel(ticker: t.toString());
|
||||
}
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// Revenue & Margins Derivation
|
||||
final totalRev = parseNullableDouble(fundMap?['totalRevenue'] ?? json['totalRevenue']);
|
||||
final grossProf = parseNullableDouble(fundMap?['grossProfit'] ?? json['grossProfit']);
|
||||
double? grossMarginVal = parseNullableDouble(fundMap?['grossMargin'] ?? json['grossMargin']);
|
||||
if (grossMarginVal == null && grossProf != null) {
|
||||
if (grossProf <= 1.0 && grossProf >= 0.0) {
|
||||
grossMarginVal = grossProf;
|
||||
} else if (totalRev != null && totalRev > 0) {
|
||||
grossMarginVal = grossProf / totalRev;
|
||||
}
|
||||
}
|
||||
|
||||
// Enterprise Value to Revenue
|
||||
final evVal = parseNullableDouble(fundMap?['enterpriseValue'] ?? json['enterpriseValue']);
|
||||
double? evToRevVal = parseNullableDouble(fundMap?['evToRevenue'] ?? fundMap?['enterpriseValueToRevenue'] ?? json['evToRevenue']);
|
||||
if (evToRevVal == null && evVal != null && totalRev != null && totalRev > 0) {
|
||||
evToRevVal = evVal / totalRev;
|
||||
}
|
||||
|
||||
// Event Dates (Ex-Dividend & Next Earnings)
|
||||
String? exDividendDateVal = json['exDividendDate']?.toString() ?? fundMap?['exDividendDate']?.toString();
|
||||
String? nextEarningsDateVal = json['nextEarningsDate']?.toString() ?? fundMap?['nextEarningsDate']?.toString();
|
||||
|
||||
final rawEvents = json['events'];
|
||||
if (rawEvents is List && rawEvents.isNotEmpty) {
|
||||
final now = DateTime.now();
|
||||
final parsedEvents = <Map<String, dynamic>>[];
|
||||
for (final ev in rawEvents) {
|
||||
if (ev is Map<String, dynamic>) {
|
||||
final dtStr = ev['date']?.toString();
|
||||
final dt = dtStr != null ? DateTime.tryParse(dtStr) : null;
|
||||
if (dt != null) {
|
||||
parsedEvents.add({
|
||||
'type': ev['type']?.toString().toUpperCase() ?? '',
|
||||
'date': dt,
|
||||
'dateStr': dtStr,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (exDividendDateVal == null) {
|
||||
final dividendEvents = parsedEvents.where((e) => e['type'] == 'DIVIDEND').toList()
|
||||
..sort((a, b) => (a['date'] as DateTime).compareTo(b['date'] as DateTime));
|
||||
final futureDividends = dividendEvents.where((e) => (e['date'] as DateTime).isAfter(now)).toList();
|
||||
if (futureDividends.isNotEmpty) {
|
||||
exDividendDateVal = futureDividends.first['dateStr'] as String;
|
||||
} else if (dividendEvents.isNotEmpty) {
|
||||
exDividendDateVal = dividendEvents.last['dateStr'] as String;
|
||||
}
|
||||
}
|
||||
|
||||
if (nextEarningsDateVal == null) {
|
||||
final earningsEvents = parsedEvents.where((e) => e['type'] == 'EARNINGS_RELEASE' || e['type'] == 'EARNINGS_CALL').toList()
|
||||
..sort((a, b) => (a['date'] as DateTime).compareTo(b['date'] as DateTime));
|
||||
final futureEarnings = earningsEvents.where((e) => (e['date'] as DateTime).isAfter(now)).toList();
|
||||
if (futureEarnings.isNotEmpty) {
|
||||
nextEarningsDateVal = futureEarnings.first['dateStr'] as String;
|
||||
} else if (earningsEvents.isNotEmpty) {
|
||||
nextEarningsDateVal = earningsEvents.last['dateStr'] as String;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return FundamentalDataModel(
|
||||
isin: json['isin']?.toString() ?? '',
|
||||
primaryTicker: json['primaryTicker']?.toString() ?? '',
|
||||
ticker: json['ticker']?.toString() ?? '',
|
||||
companyName: json['companyName']?.toString() ?? '',
|
||||
exchange: json['exchange']?.toString(),
|
||||
isin: isinVal,
|
||||
primaryTicker: primaryTickerVal,
|
||||
ticker: tickerVal,
|
||||
companyName: companyNameVal,
|
||||
exchange: exchangeVal,
|
||||
tradingCurrency: json['tradingCurrency']?.toString(),
|
||||
businessSummary: json['businessSummary']?.toString(),
|
||||
businessSummary: businessSummaryVal,
|
||||
sector: json['sector']?.toString(),
|
||||
industry: json['industry']?.toString(),
|
||||
country: json['country']?.toString(),
|
||||
@@ -143,56 +269,62 @@ class FundamentalDataModel extends Equatable {
|
||||
currentPrice: parseDouble(json['currentPrice']),
|
||||
dayChangeAbsolute: parseDouble(json['dayChangeAbsolute']),
|
||||
dayChangePercent: parseDouble(json['dayChangePercent']),
|
||||
fiftyTwoWeekHigh: parseDouble(json['fiftyTwoWeekHigh']),
|
||||
fiftyTwoWeekLow: parseDouble(json['fiftyTwoWeekLow']),
|
||||
marketCapitalization: parseDouble(json['marketCapitalization'] ?? json['marketCap']),
|
||||
enterpriseValue: parseDouble(json['enterpriseValue']),
|
||||
peRatioTrailing: parseNullableDouble(json['peRatioTrailing'] ?? json['peRatio']),
|
||||
peRatioForward: parseNullableDouble(json['peRatioForward']),
|
||||
pegRatio: parseNullableDouble(json['pegRatio']),
|
||||
pbRatio: parseNullableDouble(json['pbRatio']),
|
||||
psRatio: parseNullableDouble(json['psRatio']),
|
||||
evToEbitda: parseNullableDouble(json['evToEbitda']),
|
||||
evToRevenue: parseNullableDouble(json['evToRevenue']),
|
||||
grossMargin: parseNullableDouble(json['grossMargin']),
|
||||
operatingMargin: parseNullableDouble(json['operatingMargin']),
|
||||
netProfitMargin: parseNullableDouble(json['netProfitMargin']),
|
||||
returnOnEquity: parseNullableDouble(json['returnOnEquity']),
|
||||
returnOnAssets: parseNullableDouble(json['returnOnAssets']),
|
||||
returnOnInvestedCapital: parseNullableDouble(json['returnOnInvestedCapital']),
|
||||
debtToEquity: parseNullableDouble(json['debtToEquity']),
|
||||
currentRatio: parseNullableDouble(json['currentRatio']),
|
||||
quickRatio: parseNullableDouble(json['quickRatio']),
|
||||
interestCoverage: parseNullableDouble(json['interestCoverage']),
|
||||
dividendYield: parseNullableDouble(json['dividendYield']),
|
||||
payoutRatio: parseNullableDouble(json['payoutRatio']),
|
||||
exDividendDate: json['exDividendDate']?.toString(),
|
||||
nextEarningsDate: json['nextEarningsDate']?.toString(),
|
||||
percentHeldByInstitutions: parseNullableDouble(json['percentHeldByInstitutions']),
|
||||
percentHeldByInsiders: parseNullableDouble(json['percentHeldByInsiders']),
|
||||
shortRatio: parseNullableDouble(json['shortRatio']),
|
||||
shortPercentOfFloat: parseNullableDouble(json['shortPercentOfFloat']),
|
||||
consensusRating: json['consensusRating']?.toString(),
|
||||
priceTargetLow: parseNullableDouble(json['priceTargetLow']),
|
||||
priceTargetHigh: parseNullableDouble(json['priceTargetHigh']),
|
||||
priceTargetMedian: parseNullableDouble(json['priceTargetMedian']),
|
||||
priceTargetMean: parseNullableDouble(json['priceTargetMean']),
|
||||
fiftyTwoWeekHigh: parseDouble(fundMap?['fiftyTwoWeekHigh'] ?? json['fiftyTwoWeekHigh']),
|
||||
fiftyTwoWeekLow: parseDouble(fundMap?['fiftyTwoWeekLow'] ?? json['fiftyTwoWeekLow']),
|
||||
marketCapitalization: parseDouble(fundMap?['marketCap'] ?? json['marketCapitalization'] ?? json['marketCap']),
|
||||
enterpriseValue: parseDouble(fundMap?['enterpriseValue'] ?? json['enterpriseValue']),
|
||||
peRatioTrailing: parseNullableDouble(fundMap?['trailingPe'] ?? json['peRatioTrailing'] ?? json['peRatio']),
|
||||
peRatioForward: parseNullableDouble(fundMap?['forwardPe'] ?? json['peRatioForward']),
|
||||
pegRatio: parseNullableDouble(fundMap?['pegRatio'] ?? json['pegRatio']),
|
||||
pbRatio: parseNullableDouble(fundMap?['priceToBook'] ?? json['pbRatio']),
|
||||
psRatio: parseNullableDouble(fundMap?['priceToSales'] ?? json['psRatio']),
|
||||
evToEbitda: parseNullableDouble(fundMap?['evToEbitda'] ?? json['evToEbitda']),
|
||||
evToRevenue: evToRevVal,
|
||||
totalRevenue: totalRev,
|
||||
revenueGrowthYoY: parseNullableDouble(fundMap?['revenueGrowthYoY'] ?? json['revenueGrowthYoY']),
|
||||
grossProfit: grossProf,
|
||||
ebitda: parseNullableDouble(fundMap?['ebitda'] ?? json['ebitda']),
|
||||
dilutedEps: parseNullableDouble(fundMap?['dilutedEps'] ?? json['dilutedEps']),
|
||||
totalCash: parseNullableDouble(fundMap?['totalCash'] ?? json['totalCash']),
|
||||
totalDebt: parseNullableDouble(fundMap?['totalDebt'] ?? json['totalDebt']),
|
||||
operatingCashFlow: parseNullableDouble(fundMap?['operatingCashFlow'] ?? json['operatingCashFlow']),
|
||||
freeCashFlow: parseNullableDouble(fundMap?['freeCashFlow'] ?? json['freeCashFlow']),
|
||||
grossMargin: grossMarginVal,
|
||||
operatingMargin: parseNullableDouble(fundMap?['operatingMargin'] ?? fundMap?['operatingIncome'] ?? json['operatingMargin']),
|
||||
netProfitMargin: parseNullableDouble(fundMap?['netProfitMargin'] ?? fundMap?['netIncome'] ?? json['netProfitMargin']),
|
||||
returnOnEquity: parseNullableDouble(fundMap?['returnOnEquity'] ?? json['returnOnEquity']),
|
||||
returnOnAssets: parseNullableDouble(fundMap?['returnOnAssets'] ?? json['returnOnAssets']),
|
||||
returnOnInvestedCapital: parseNullableDouble(fundMap?['returnOnInvestedCapital'] ?? json['returnOnInvestedCapital']),
|
||||
debtToEquity: parseNullableDouble(fundMap?['debtToEquity'] ?? json['debtToEquity']),
|
||||
currentRatio: parseNullableDouble(fundMap?['currentRatio'] ?? json['currentRatio']),
|
||||
quickRatio: parseNullableDouble(fundMap?['quickRatio'] ?? json['quickRatio']),
|
||||
interestCoverage: parseNullableDouble(fundMap?['interestCoverage'] ?? json['interestCoverage']),
|
||||
dividendYield: parseNullableDouble(fundMap?['forwardDividendYield'] ?? json['dividendYield']),
|
||||
payoutRatio: parseNullableDouble(fundMap?['payoutRatio'] ?? json['payoutRatio']),
|
||||
exDividendDate: exDividendDateVal,
|
||||
nextEarningsDate: nextEarningsDateVal,
|
||||
percentHeldByInstitutions: parseNullableDouble(fundMap?['percentHeldByInstitutions'] ?? json['percentHeldByInstitutions']),
|
||||
percentHeldByInsiders: parseNullableDouble(fundMap?['percentHeldByInsiders'] ?? json['percentHeldByInsiders']),
|
||||
shortRatio: parseNullableDouble(fundMap?['shortRatio'] ?? json['shortRatio']),
|
||||
shortPercentOfFloat: parseNullableDouble(fundMap?['shortPercentOfFloat'] ?? json['shortPercentOfFloat']),
|
||||
consensusRating: (fundMap?['consensusRating'] ?? json['consensusRating'])?.toString(),
|
||||
priceTargetLow: parseNullableDouble(fundMap?['priceTargetLow'] ?? json['priceTargetLow']),
|
||||
priceTargetHigh: parseNullableDouble(fundMap?['priceTargetHigh'] ?? json['priceTargetHigh']),
|
||||
priceTargetMedian: parseNullableDouble(fundMap?['priceTargetMedian'] ?? json['priceTargetMedian']),
|
||||
priceTargetMean: parseNullableDouble(fundMap?['priceTargetMean'] ?? json['priceTargetMean']),
|
||||
executives: (json['executives'] as List?)
|
||||
?.map((e) => CompanyExecutiveModel.fromJson(e))
|
||||
?.map((e) => CompanyExecutiveModel.fromJson(e is Map<String, dynamic> ? e : {}))
|
||||
.toList() ??
|
||||
[],
|
||||
financialStatements: (json['financialStatements'] as List?)
|
||||
?.map((e) => FinancialStatementModel.fromJson(e))
|
||||
?.map((e) => FinancialStatementModel.fromJson(e is Map<String, dynamic> ? e : {}))
|
||||
.toList() ??
|
||||
[],
|
||||
estimates: (json['estimates'] as List?)
|
||||
?.map((e) => ForwardEstimateModel.fromJson(e))
|
||||
.toList() ??
|
||||
[],
|
||||
availableTickers: (json['availableTickers'] as List?)
|
||||
?.map((e) => TickerModel.fromJson(e))
|
||||
?.map((e) => ForwardEstimateModel.fromJson(e is Map<String, dynamic> ? e : {}))
|
||||
.toList() ??
|
||||
[],
|
||||
availableTickers: availableTickersList,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -322,11 +454,30 @@ class CompanyExecutiveModel extends Equatable {
|
||||
});
|
||||
|
||||
factory CompanyExecutiveModel.fromJson(Map<String, dynamic> json) {
|
||||
double? compVal;
|
||||
if (json['compensation'] != null) {
|
||||
compVal = double.tryParse(json['compensation'].toString());
|
||||
} else if (json['payment'] != null) {
|
||||
final pStr = json['payment'].toString().trim().toUpperCase().replaceAll('\$', '').replaceAll('€', '').replaceAll('£', '').replaceAll(',', '').replaceAll(' ', '');
|
||||
if (pStr.endsWith('M')) {
|
||||
final numPart = double.tryParse(pStr.substring(0, pStr.length - 1));
|
||||
if (numPart != null) compVal = numPart * 1e6;
|
||||
} else if (pStr.endsWith('K')) {
|
||||
final numPart = double.tryParse(pStr.substring(0, pStr.length - 1));
|
||||
if (numPart != null) compVal = numPart * 1e3;
|
||||
} else if (pStr.endsWith('B')) {
|
||||
final numPart = double.tryParse(pStr.substring(0, pStr.length - 1));
|
||||
if (numPart != null) compVal = numPart * 1e9;
|
||||
} else {
|
||||
compVal = double.tryParse(pStr);
|
||||
}
|
||||
}
|
||||
|
||||
return CompanyExecutiveModel(
|
||||
name: json['name']?.toString() ?? '',
|
||||
title: json['title']?.toString() ?? '',
|
||||
age: json['age'] != null ? int.tryParse(json['age'].toString()) : null,
|
||||
compensation: json['compensation'] != null ? double.tryParse(json['compensation'].toString()) : null,
|
||||
compensation: compVal,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -545,7 +696,7 @@ class TickerModel extends Equatable {
|
||||
required this.ticker,
|
||||
this.exchange,
|
||||
this.tradingCurrency,
|
||||
required this.currentPrice,
|
||||
this.currentPrice = 0.0,
|
||||
});
|
||||
|
||||
factory TickerModel.fromJson(Map<String, dynamic> json) {
|
||||
|
||||
Reference in New Issue
Block a user