diff --git a/sigap-mobile/lib/src/features/panic/presentation/widgets/statistics_view.dart b/sigap-mobile/lib/src/features/panic/presentation/widgets/statistics_view.dart index 4cd3672..9937ed8 100644 --- a/sigap-mobile/lib/src/features/panic/presentation/widgets/statistics_view.dart +++ b/sigap-mobile/lib/src/features/panic/presentation/widgets/statistics_view.dart @@ -2,8 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:sigap/src/features/panic/presentation/controllers/main_safety_indicator_controller.dart'; import 'package:sigap/src/features/panic/presentation/controllers/statistics_view_controller.dart'; -import 'package:sigap/src/shared/widgets/loaders/custom_circular_loader.dart'; -import 'package:sigap/src/utils/constants/colors.dart'; +import 'package:sigap/src/utils/loaders/shimmer.dart'; import 'crime_stats_header.dart'; import 'main_safety_indicator.dart'; @@ -24,81 +23,81 @@ class StatisticsView extends StatelessWidget { // Main content SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // District and date information - CrimeStatsHeader( - district: - statsController.currentDistrictId.value.isNotEmpty - ? _getDistrictName( - statsController.currentDistrictId.value, - ) - : _getLocalityName(), - month: statsController.currentMonth.value, - year: statsController.currentYear.value, - onMonthChanged: statsController.changeMonth, - onYearChanged: statsController.changeYear, - onRefresh: statsController.refreshStatistics, - ), + child: + statsController.isLoading.value + ? _buildShimmerLoading(context) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // District and date information + CrimeStatsHeader( + district: + statsController.currentDistrictId.value.isNotEmpty + ? _getDistrictName( + statsController.currentDistrictId.value, + ) + : _getLocalityName(), + month: statsController.currentMonth.value, + year: statsController.currentYear.value, + onMonthChanged: statsController.changeMonth, + onYearChanged: statsController.changeYear, + onRefresh: statsController.refreshStatistics, + ), - const SizedBox(height: 15), + const SizedBox(height: 15), - // Main indicator - Area Safety Level - MainSafetyIndicator( - progress: safetyController.progress.value, - title: safetyController.title.value, - label: safetyController.label.value, - color: _getSafetyColor(safetyController.progress.value), - ), + // Main indicator - Area Safety Level + MainSafetyIndicator( + progress: safetyController.progress.value, + title: safetyController.title.value, + label: safetyController.label.value, + color: _getSafetyColor( + safetyController.progress.value, + ), + ), - const SizedBox(height: 15), + const SizedBox(height: 15), - // Secondary indicators row with donut charts - Row( - children: [ - Expanded( - child: StatIndicatorCard( - progress: statsController.reportsProgress.value, - value: statsController.reportsValue.value, - label: 'Reports', - color: Colors.teal, - ), + // Secondary indicators row with donut charts + Row( + children: [ + Expanded( + child: StatIndicatorCard( + progress: statsController.reportsProgress.value, + value: statsController.reportsValue.value, + label: 'Reports', + color: Colors.teal, + ), + ), + const SizedBox(width: 10), + Expanded( + child: StatIndicatorCard( + progress: statsController.zoneMinProgress.value, + value: statsController.zoneMinValue.value, + label: 'Safety Score', + color: Colors.green, + ), + ), + const SizedBox(width: 10), + Expanded( + child: StatIndicatorCard( + progress: statsController.mindfulProgress.value, + value: statsController.mindfulValue.value, + label: 'Solved Rate', + color: Colors.indigo, + ), + ), + ], + ), + + const SizedBox(height: 15), + + // Recovery indicator with unverified incidents + const RecoveryIndicator(), + ], ), - const SizedBox(width: 10), - Expanded( - child: StatIndicatorCard( - progress: statsController.zoneMinProgress.value, - value: statsController.zoneMinValue.value, - label: 'Safety Score', - color: Colors.green, - ), - ), - const SizedBox(width: 10), - Expanded( - child: StatIndicatorCard( - progress: statsController.mindfulProgress.value, - value: statsController.mindfulValue.value, - label: 'Solved Rate', - color: Colors.indigo, - ), - ), - ], - ), - - const SizedBox(height: 15), - - // Recovery indicator with unverified incidents - const RecoveryIndicator(), - ], - ), ), - // Loading indicator - if (statsController.isLoading.value) - Center(child: TLoader.centeredLoader(color: TColors.primary), - ), - // Error message if (statsController.errorMessage.value.isNotEmpty) Positioned( @@ -124,6 +123,168 @@ class StatisticsView extends StatelessWidget { ); } + // Build shimmer loading placeholders + Widget _buildShimmerLoading(BuildContext context) { + return Column( + children: [ + // Header shimmer + _buildHeaderShimmer(), + const SizedBox(height: 15), + + // Main indicator shimmer + _buildMainIndicatorShimmer(), + const SizedBox(height: 15), + + // Stats cards shimmer + Row( + children: [ + Expanded(child: _buildStatCardShimmer()), + const SizedBox(width: 10), + Expanded(child: _buildStatCardShimmer()), + const SizedBox(width: 10), + Expanded(child: _buildStatCardShimmer()), + ], + ), + const SizedBox(height: 15), + + // Recovery indicator shimmer + _buildRecoveryShimmer(), + ], + ); + } + + // Header shimmer placeholder + Widget _buildHeaderShimmer() { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(15), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 5, + spreadRadius: 1, + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const TShimmerEffect(width: 150, height: 20, radius: 4), + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: Colors.grey.withOpacity(0.2), + shape: BoxShape.circle, + ), + ), + ], + ), + const SizedBox(height: 15), + Row( + children: const [ + TShimmerEffect(width: 120, height: 30, radius: 5), + SizedBox(width: 10), + TShimmerEffect(width: 80, height: 30, radius: 5), + ], + ), + ], + ), + ); + } + + // Main safety indicator shimmer + Widget _buildMainIndicatorShimmer() { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 5, + spreadRadius: 1, + ), + ], + ), + child: Column( + children: [ + const TShimmerEffect(width: 120, height: 120, radius: 60), // Circle + const SizedBox(height: 15), + const TShimmerEffect(width: 100, height: 20, radius: 4), // Text + ], + ), + ); + } + + // Stat card shimmer + Widget _buildStatCardShimmer() { + return Container( + padding: const EdgeInsets.all(15), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 5, + spreadRadius: 1, + ), + ], + ), + child: Column( + children: const [ + TShimmerEffect(width: 60, height: 60, radius: 30), // Circle + SizedBox(height: 10), + TShimmerEffect(width: 40, height: 18, radius: 4), // Value + SizedBox(height: 5), + TShimmerEffect(width: 60, height: 14, radius: 4), // Label + ], + ), + ); + } + + // Recovery indicator shimmer + Widget _buildRecoveryShimmer() { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(15), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 5, + spreadRadius: 1, + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + TShimmerEffect(width: 140, height: 20, radius: 4), // Title + SizedBox(height: 15), + TShimmerEffect( + width: double.infinity, + height: 12, + radius: 6, + ), // Progress bar + SizedBox(height: 10), + TShimmerEffect(width: 80, height: 16, radius: 4), // Status text + ], + ), + ); + } + // Helper to get district name from district ID String _getDistrictName(String districtId) { final statsController = Get.find();