import 'dart:convert'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:http/http.dart' as http; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:nearle/Models/summary/riderweeklykms.dart'; import 'package:nearle/views/helpers/constants/Font_constant.dart'; import 'package:nearle/controllers/summary_controller.dart'; import 'package:shared_preferences/shared_preferences.dart'; class Summary extends StatefulWidget { const Summary({super.key}); @override State createState() => _SummaryState(); } class _SummaryState extends State { final SummaryController controller = Get.put(SummaryController()); int _userId = 0; int _refreshTick = 0; @override void initState() { super.initState(); _refreshData(); } Future _refreshData() async { try { final prefs = await SharedPreferences.getInstance(); final uid = prefs.getInt('userid') ?? prefs.getInt('userId') ?? 0; if (mounted) { setState(() { _userId = uid; }); } if (uid > 0) { await controller.fetchSummaryStats(uid); } } catch (e) { debugPrint("❌ Error fetching summary: $e"); } finally { if (mounted) { setState(() { _refreshTick++; }); } } } // ------------------------ // RESPONSIVE CARD // ------------------------ Widget _buildCard({ required String title, required String value, required String imagePath, bool isCancelled = false, }) { return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12.r), border: Border.all( color: isCancelled ? const Color(0xFFFF5C5C) : const Color.fromARGB(255, 159, 139, 163), width: 1.2.w, ), ), child: Padding( padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 14.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Image.asset( imagePath, height: 36.h, width: 36.w, color: isCancelled ? const Color(0xFFFF5C5C) : const Color(0xFF9C27B0), ), SizedBox(width: 10.w), Expanded( child: Text( title, style: TextStyle( fontFamily: FontConstants.fontFamily, color: Colors.grey.shade700, fontSize: 20.sp, fontWeight: FontWeight.w400, ), ), ), ], ), SizedBox(height: 30.h), Text( value, style: TextStyle( fontFamily: FontConstants.fontFamily, fontSize: 29.sp, fontWeight: FontWeight.w600, color: Colors.black, ), ), ], ), ), ); } // ------------------------ // RESPONSIVE CANCELLED CARD // ------------------------ Widget _buildCancelledCard(String value) { return Container( width: double.infinity, decoration: BoxDecoration( color: Colors.white, border: Border.all( color: const Color.fromARGB(255, 232, 167, 167), width: 1.3.w, ), borderRadius: BorderRadius.circular(10.r), ), padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), child: Row( children: [ Image.asset( 'assets/images/cancel.png', height: 36.h, width: 36.w, color: const Color(0xFFFF5C5C), ), SizedBox(width: 15.w), Expanded( child: Text( 'Cancelled Orders', style: TextStyle( fontFamily: FontConstants.fontFamily, color: Colors.grey.shade700, fontSize: 20.sp, fontWeight: FontWeight.w400, ), ), ), Text( value, style: TextStyle( fontFamily: FontConstants.fontFamily, fontSize: 34.sp, fontWeight: FontWeight.w600, color: Colors.black, ), ), ], ), ); } // ------------------------ // MAIN UI // ------------------------ @override Widget build(BuildContext context) { return SafeArea( child: Scaffold( backgroundColor: Colors.grey.shade200, appBar: AppBar( backgroundColor: Colors.grey.shade200, elevation: 0, centerTitle: false, toolbarHeight: 70.h, title: Padding( padding: EdgeInsets.only(top: 12.h), child: Text( "SUMMARY", style: TextStyle( fontSize: FontConstants.xxxLarge(context).sp, fontWeight: FontWeight.bold, fontFamily: FontConstants.fontFamily, color: Colors.black, ), ), ), bottom: PreferredSize( preferredSize: Size.fromHeight(1.h), child: Divider(height: 1.h, color: Colors.grey), ), ), body: Obx(() { return RefreshIndicator( onRefresh: _refreshData, child: SingleChildScrollView( physics: const ClampingScrollPhysics(), padding: EdgeInsets.all(16.r), child: Column( children: [ GridView.count( crossAxisCount: 2, crossAxisSpacing: 12.w, mainAxisSpacing: 12.h, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), children: [ _buildCard( title: 'Today', value: controller.today.value.toString(), imagePath: 'assets/images/today.png', ), _buildCard( title: 'Week', value: controller.week.value.toString(), imagePath: 'assets/images/week.png', ), _buildCard( title: 'Month', value: controller.month.value.toString(), imagePath: 'assets/images/week.png', ), _buildCard( title: 'Total', value: controller.total.value.toString(), imagePath: 'assets/images/total.png', ), ], ), SizedBox(height: 12.h), _buildCancelledCard(controller.cancelled.value.toString()), SizedBox(height: 12.h), Row( children: [ Text( "Statistics", style: TextStyle( fontSize: 26.sp, fontWeight: FontWeight.bold, fontFamily: FontConstants.fontFamily, color: Colors.black87, ), ), ], ), SizedBox(height: 10.h), TotalDistanceCard(userId: _userId, refreshTick: _refreshTick), ], ), ), ); }), ), ); } } // ======================================================== // RESPONSIVE GRAPH CARD // ======================================================== class TotalDistanceCard extends StatefulWidget { final int userId; final int refreshTick; const TotalDistanceCard({ super.key, required this.userId, this.refreshTick = 0, }); @override State createState() => _TotalDistanceCardState(); } class _TotalDistanceCardState extends State { late Future> _futureKms; @override void initState() { super.initState(); _futureKms = _fetchWeeklyKms(); } @override void didUpdateWidget(covariant TotalDistanceCard oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.userId != widget.userId || oldWidget.refreshTick != widget.refreshTick) { setState(() { _futureKms = _fetchWeeklyKms(); }); } } double _toDouble(dynamic v) { if (v == null) return 0.0; if (v is num) return v.toDouble(); return double.tryParse(v.toString()) ?? 0.0; } Future> _fetchWeeklyKms() async { if (widget.userId == 0) { return {'details': [], 'total_kms': 0.0}; } try { final uri = Uri.parse( 'https://jupiter.nearle.app/live/api/v1/partners/getriderweeklykms?userid=${widget.userId}', ); final response = await http.get(uri); if (response.statusCode == 200) { final data = json.decode(response.body); if (data is Map && data['status'] == true) { final rawDetails = (data['details'] is List) ? data['details'] as List : const []; final details = rawDetails .map((e) => RiderWeeklyKms.fromJson(e)) .toList(); final total = _toDouble(data['total_kms']); return {'details': details, 'total_kms': total}; } } } catch (e) { debugPrint('❌ _fetchWeeklyKms Error: $e'); } return {'details': [], 'total_kms': 0.0}; } @override Widget build(BuildContext context) { final size = MediaQuery.of(context).size; return FutureBuilder>( future: _futureKms, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return Padding( padding: EdgeInsets.symmetric(vertical: 24.h), child: const Center( child: CircularProgressIndicator(color: Colors.deepPurple), ), ); } final List details = snapshot.data?['details'] ?? []; final double totalKms = snapshot.data?['total_kms'] ?? 0.0; return _buildDistanceCard(details, totalKms, size); }, ); } Widget _buildDistanceCard( List details, double totalKms, Size size, ) { final double maxY = details.isEmpty ? 10 : _getMaxY(details); final double chartHeight = (size.height * 0.25).clamp(160.h, 280.h); final double leftInterval = _calculateInterval(maxY); final double maxK = _getMaxKms(details); return Container( width: double.infinity, margin: EdgeInsets.only(top: 4.h), decoration: BoxDecoration( color: Colors.white, border: Border.all( color: const Color.fromARGB(255, 222, 161, 235), width: 1.3.w, ), borderRadius: BorderRadius.circular(10.r), ), padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Total Distance", style: TextStyle( fontSize: 20.sp, fontWeight: FontWeight.w700, color: Colors.black, ), ), Text( "${totalKms.toStringAsFixed(2)} Km", style: TextStyle( fontSize: 20.sp, fontWeight: FontWeight.w700, color: Colors.black, ), ), ], ), SizedBox(height: 30.h), if (details.isEmpty) Padding( padding: EdgeInsets.symmetric(vertical: 8.h), child: Center( child: Text( "No weekly data available", style: TextStyle(color: Colors.grey, fontSize: 16.sp), ), ), ), SizedBox( height: chartHeight, width: double.infinity, child: BarChart( BarChartData( maxY: maxY, gridData: FlGridData( show: true, drawVerticalLine: false, getDrawingHorizontalLine: (value) => FlLine( color: Colors.grey.withOpacity(0.12), strokeWidth: 1, ), ), borderData: FlBorderData(show: false), alignment: BarChartAlignment.spaceAround, titlesData: FlTitlesData( topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), rightTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 60.w, interval: leftInterval, getTitlesWidget: (value, _) => Text( "${value.toInt()} km", style: TextStyle( fontSize: 14.sp, color: Colors.black87, ), ), ), ), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: (value, _) { final idx = value.toInt(); if (idx >= 0 && idx < details.length) { return Text( details[idx].day, style: TextStyle( fontSize: 14.sp, color: Colors.black87, ), ); } return const SizedBox.shrink(); }, ), ), ), barGroups: List.generate(details.isEmpty ? 7 : details.length, ( i, ) { final kms = details.isEmpty ? 0.0 : details[i].kms.toDouble(); return BarChartGroupData( x: i, barRods: [ BarChartRodData( toY: kms, color: (details.isNotEmpty && kms == maxK) ? const Color(0xFF8124DB) : const Color(0xFFB274F3), width: 20.w, borderRadius: BorderRadius.circular(6.r), ), ], ); }), barTouchData: BarTouchData( enabled: true, touchTooltipData: BarTouchTooltipData( tooltipPadding: EdgeInsets.symmetric( horizontal: 12.w, vertical: 8.h, ), getTooltipItem: (group, index, rod, rodIndex) { return BarTooltipItem( "${rod.toY.toStringAsFixed(2)} Km", TextStyle( color: Colors.white, fontSize: 18.sp, fontWeight: FontWeight.bold, ), ); }, ), ), ), ), ), ], ), ); } double _getMaxY(List details) { if (details.isEmpty) return 10; double maxVal = details.map((e) => e.kms).reduce(max); if (maxVal <= 5) return 10; final double withPadding = maxVal * 1.2; return _roundUpNice(withPadding); } double _roundUpNice(double v) { final exponent = pow(10, (log(v) / ln10).floor()); final mantissa = v / exponent; double niceMantissa; if (mantissa <= 1) { niceMantissa = 1; } else if (mantissa <= 2) niceMantissa = 2; else if (mantissa <= 5) niceMantissa = 5; else niceMantissa = 10; return (niceMantissa * exponent).ceilToDouble(); } double _calculateInterval(double maxY) { const int desiredTicks = 5; double rough = max(1, (maxY / desiredTicks)); final exponent = pow(10, (log(rough) / ln10).floor()); final mantissa = rough / exponent; double niceMantissa; if (mantissa <= 1) { niceMantissa = 1; } else if (mantissa <= 2) niceMantissa = 2; else if (mantissa <= 5) niceMantissa = 5; else niceMantissa = 10; return (niceMantissa * exponent).toDouble(); } double _getMaxKms(List details) { if (details.isEmpty) return 0; return details.map((e) => e.kms).reduce(max); } }