indicators
This commit is contained in:
parent
820d0e4982
commit
b927666c12
@ -580,12 +580,21 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
|
||||
final firstResponse =
|
||||
responses.isNotEmpty ? responses.first : <String, dynamic>{};
|
||||
final secondResponse = responses.length > 1 ? responses[1] : null;
|
||||
final trendText = _textValue(secondResponse?['value']);
|
||||
return _IndicatorItem(
|
||||
title: _textValue(entry['chart_heading']),
|
||||
value: _textValue(firstResponse['value']),
|
||||
period: _textValue(firstResponse['display_value']),
|
||||
trendText: _textValue(secondResponse?['value']),
|
||||
trendText: trendText,
|
||||
trendPeriod: _textValue(secondResponse?['display_value']),
|
||||
icon: _indicatorItemIcon(
|
||||
topicName,
|
||||
subTopicName,
|
||||
_textValue(entry['chart_heading']),
|
||||
_textValue(entry['kpi']),
|
||||
),
|
||||
sparklineValues: _sparklineFromResponses(responses),
|
||||
isTrendUp: _isPositiveTrend(trendText),
|
||||
);
|
||||
}).toList();
|
||||
|
||||
@ -616,6 +625,104 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
|
||||
return double.tryParse(cleaned);
|
||||
}
|
||||
|
||||
bool _isPositiveTrend(String trendText) {
|
||||
if (trendText.isEmpty) return true;
|
||||
return !trendText.trim().startsWith('-');
|
||||
}
|
||||
|
||||
List<double> _sparklineFromResponses(List<Map<String, dynamic>> responses) {
|
||||
final values = <double>[];
|
||||
for (final response in responses) {
|
||||
final parsed = _parseNumericTrend(_textValue(response['value']));
|
||||
if (parsed != null) {
|
||||
values.add(parsed);
|
||||
}
|
||||
}
|
||||
if (values.length < 2) return const [];
|
||||
|
||||
final minValue = values.reduce(math.min);
|
||||
final maxValue = values.reduce(math.max);
|
||||
if (maxValue == minValue) {
|
||||
return List<double>.filled(values.length, 0.5);
|
||||
}
|
||||
return values
|
||||
.map((value) => (value - minValue) / (maxValue - minValue))
|
||||
.toList();
|
||||
}
|
||||
|
||||
IconData _indicatorItemIcon(
|
||||
String topicName,
|
||||
String subTopicName,
|
||||
String heading,
|
||||
String kpi,
|
||||
) {
|
||||
final normalizedHeading = _normalizeKey(heading);
|
||||
final normalizedKpi = _normalizeKey(kpi);
|
||||
|
||||
if (normalizedHeading.contains('male') ||
|
||||
normalizedKpi.contains('male')) {
|
||||
return Icons.person_outline;
|
||||
}
|
||||
if (normalizedHeading.contains('female') ||
|
||||
normalizedKpi.contains('female')) {
|
||||
return Icons.person_outline;
|
||||
}
|
||||
if (normalizedHeading.contains('enrollment') ||
|
||||
normalizedHeading.contains('student') ||
|
||||
normalizedKpi.contains('student')) {
|
||||
return Icons.school_outlined;
|
||||
}
|
||||
if (normalizedHeading.contains('teacher') ||
|
||||
normalizedKpi.contains('teacher')) {
|
||||
return Icons.menu_book_outlined;
|
||||
}
|
||||
if (normalizedHeading.contains('hospital') ||
|
||||
normalizedKpi.contains('hospital')) {
|
||||
return Icons.local_hospital_outlined;
|
||||
}
|
||||
if (normalizedHeading.contains('bed') ||
|
||||
normalizedKpi.contains('bed')) {
|
||||
return Icons.hotel_outlined;
|
||||
}
|
||||
if (normalizedHeading.contains('health') ||
|
||||
normalizedKpi.contains('health')) {
|
||||
return Icons.medical_services_outlined;
|
||||
}
|
||||
if (normalizedHeading.contains('government')) {
|
||||
return Icons.account_balance_outlined;
|
||||
}
|
||||
if (normalizedHeading.contains('private')) {
|
||||
return Icons.business_outlined;
|
||||
}
|
||||
if (normalizedHeading.contains('growth') ||
|
||||
normalizedHeading.contains('rate')) {
|
||||
return Icons.trending_up_rounded;
|
||||
}
|
||||
if (normalizedHeading.contains('gdp')) {
|
||||
return Icons.show_chart_rounded;
|
||||
}
|
||||
if (normalizedHeading.contains('import') ||
|
||||
normalizedHeading.contains('export') ||
|
||||
normalizedHeading.contains('trade')) {
|
||||
return Icons.swap_horiz_rounded;
|
||||
}
|
||||
if (normalizedHeading.contains('temperature') ||
|
||||
normalizedHeading.contains('climate')) {
|
||||
return Icons.thermostat_outlined;
|
||||
}
|
||||
if (normalizedHeading.contains('electric') ||
|
||||
normalizedHeading.contains('consumption') ||
|
||||
normalizedHeading.contains('generation')) {
|
||||
return Icons.bolt_outlined;
|
||||
}
|
||||
if (normalizedHeading.contains('crop') ||
|
||||
normalizedHeading.contains('land')) {
|
||||
return Icons.grass_outlined;
|
||||
}
|
||||
|
||||
return _sectionTileIcon(topicName, subTopicName);
|
||||
}
|
||||
|
||||
List<_HomeSectionGroup> _buildDynamicTopicGroups(
|
||||
Map<String, dynamic>? topic) {
|
||||
final topicName = _textValue(topic?['main_topic']);
|
||||
@ -1132,6 +1239,9 @@ class _IndicatorItem {
|
||||
required this.period,
|
||||
this.trendText = '',
|
||||
this.trendPeriod = '',
|
||||
this.icon = Icons.insights_outlined,
|
||||
this.sparklineValues = const [],
|
||||
this.isTrendUp = true,
|
||||
});
|
||||
|
||||
final String title;
|
||||
@ -1139,6 +1249,9 @@ class _IndicatorItem {
|
||||
final String period;
|
||||
final String trendText;
|
||||
final String trendPeriod;
|
||||
final IconData icon;
|
||||
final List<double> sparklineValues;
|
||||
final bool isTrendUp;
|
||||
}
|
||||
|
||||
// ── Hero card ─────────────────────────────────────────────────────────────────
|
||||
@ -1210,8 +1323,8 @@ class _HeroCard extends ConsumerWidget {
|
||||
Flexible(
|
||||
child: Text(
|
||||
isArabic
|
||||
? 'تحديث · ١٥ مايو ٢٠٢٦'
|
||||
: 'Updated · 15 May 2026',
|
||||
? 'تحديث · ٠١ مارس ٢٠٢٦'
|
||||
: 'Updated · 01 Mar 2026',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
@ -2264,7 +2377,11 @@ class _IndicatorsSheet extends StatelessWidget {
|
||||
const Divider(height: 1, indent: 20, color: _kPearl),
|
||||
itemBuilder: (_, index) {
|
||||
final indicator = indicators[index];
|
||||
return _IndicatorRow(item: indicator, accentColor: iconColor);
|
||||
return _IndicatorRow(
|
||||
item: indicator,
|
||||
accentColor: iconColor,
|
||||
accentBg: iconBg,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
@ -2276,17 +2393,36 @@ class _IndicatorsSheet extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _IndicatorRow extends StatelessWidget {
|
||||
const _IndicatorRow({required this.item, required this.accentColor});
|
||||
const _IndicatorRow({
|
||||
required this.item,
|
||||
required this.accentColor,
|
||||
required this.accentBg,
|
||||
});
|
||||
|
||||
final _IndicatorItem item;
|
||||
final Color accentColor;
|
||||
final Color accentBg;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final up = item.isTrendUp;
|
||||
final prevPeriod = item.period.isNotEmpty ? item.period : '—';
|
||||
final vsLabel = item.trendPeriod.isNotEmpty ? item.trendPeriod : prevPeriod;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: accentBg,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(item.icon, size: 18, color: accentColor),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -2299,13 +2435,13 @@ class _IndicatorRow extends StatelessWidget {
|
||||
color: _kSlate900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
if (item.period.isNotEmpty)
|
||||
Text(
|
||||
item.period,
|
||||
style: const TextStyle(fontSize: 11, color: _kSlate400),
|
||||
style:
|
||||
const TextStyle(fontSize: 11, color: _kSlate400),
|
||||
),
|
||||
if (item.trendText.isNotEmpty) ...[
|
||||
const SizedBox(width: 8),
|
||||
@ -2313,19 +2449,44 @@ class _IndicatorRow extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.successBg,
|
||||
color: up ? AppColors.successBg : AppColors.errorBg,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Text(
|
||||
item.trendText +
|
||||
(item.trendPeriod.isNotEmpty
|
||||
? ' vs ${item.trendPeriod}'
|
||||
: ''),
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.success,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
up
|
||||
? Icons.arrow_upward_rounded
|
||||
: Icons.arrow_downward_rounded,
|
||||
size: 9,
|
||||
color:
|
||||
up ? AppColors.success : AppColors.error,
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
Text(
|
||||
item.trendText.contains('%')
|
||||
? item.trendText
|
||||
: '${item.trendText}%',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: up
|
||||
? AppColors.success
|
||||
: AppColors.error,
|
||||
),
|
||||
),
|
||||
if (vsLabel.isNotEmpty) ...[
|
||||
const SizedBox(width: 3),
|
||||
Text(
|
||||
'vs $vsLabel',
|
||||
style: const TextStyle(
|
||||
fontSize: 9,
|
||||
color: _kSlate400,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -2334,18 +2495,34 @@ class _IndicatorRow extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
item.value.isEmpty ? '—' : item.value,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: accentColor,
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
item.value.isEmpty ? '—' : item.value,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: _kSlate900,
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 60,
|
||||
height: 18,
|
||||
child: CustomPaint(
|
||||
painter: _SparklinePainter(
|
||||
points: item.sparklineValues,
|
||||
isUp: up,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Icon(Icons.arrow_forward_ios_rounded, size: 13, color: _kSlate400),
|
||||
const Icon(Icons.arrow_forward_ios_rounded,
|
||||
size: 13, color: _kSlate400),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user