initial commit: push everything
This commit is contained in:
587
lib/views/Dashboard/summary/summary.dart
Normal file
587
lib/views/Dashboard/summary/summary.dart
Normal file
@@ -0,0 +1,587 @@
|
||||
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<Summary> createState() => _SummaryState();
|
||||
}
|
||||
|
||||
class _SummaryState extends State<Summary> {
|
||||
final SummaryController controller = Get.put(SummaryController());
|
||||
int _userId = 0;
|
||||
int _refreshTick = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_refreshData();
|
||||
}
|
||||
|
||||
Future<void> _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<TotalDistanceCard> createState() => _TotalDistanceCardState();
|
||||
}
|
||||
|
||||
class _TotalDistanceCardState extends State<TotalDistanceCard> {
|
||||
late Future<Map<String, dynamic>> _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<Map<String, dynamic>> _fetchWeeklyKms() async {
|
||||
if (widget.userId == 0) {
|
||||
return {'details': <RiderWeeklyKms>[], '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': <RiderWeeklyKms>[], 'total_kms': 0.0};
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
|
||||
return FutureBuilder<Map<String, dynamic>>(
|
||||
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<RiderWeeklyKms> details =
|
||||
snapshot.data?['details'] ?? <RiderWeeklyKms>[];
|
||||
|
||||
final double totalKms = snapshot.data?['total_kms'] ?? 0.0;
|
||||
|
||||
return _buildDistanceCard(details, totalKms, size);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDistanceCard(
|
||||
List<RiderWeeklyKms> 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<RiderWeeklyKms> 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<RiderWeeklyKms> details) {
|
||||
if (details.isEmpty) return 0;
|
||||
return details.map((e) => e.kms).reduce(max);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user