From b927666c12d025bcbbc07455266297f10fbc6798 Mon Sep 17 00:00:00 2001 From: SurendarSuri30 Date: Fri, 29 May 2026 17:58:51 +0530 Subject: [PATCH] indicators --- .../presentation/screens/home_screen.dart | 233 +++++++++++++++--- 1 file changed, 205 insertions(+), 28 deletions(-) diff --git a/lib/lib/features/home/presentation/screens/home_screen.dart b/lib/lib/features/home/presentation/screens/home_screen.dart index 82619253..800dbb79 100644 --- a/lib/lib/features/home/presentation/screens/home_screen.dart +++ b/lib/lib/features/home/presentation/screens/home_screen.dart @@ -580,12 +580,21 @@ class _HomeScreenState extends ConsumerState { final firstResponse = responses.isNotEmpty ? responses.first : {}; 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 { return double.tryParse(cleaned); } + bool _isPositiveTrend(String trendText) { + if (trendText.isEmpty) return true; + return !trendText.trim().startsWith('-'); + } + + List _sparklineFromResponses(List> responses) { + final values = []; + 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.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? 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 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), ], ), );