825 lines
32 KiB
Dart
825 lines
32 KiB
Dart
import 'package:fl_chart/fl_chart.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:intl/intl.dart' hide TextDirection;
|
|
import 'package:sefer_admin1/constant/colors.dart';
|
|
import 'package:sefer_admin1/controller/admin/static_controller.dart';
|
|
import 'package:sefer_admin1/views/widgets/mycircular.dart';
|
|
// import صفحة الملاحظات الجديدة
|
|
import 'notes_driver_page.dart';
|
|
|
|
class StaticDash extends StatelessWidget {
|
|
const StaticDash({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final controller = Get.put(StaticController());
|
|
bool isRtl = Directionality.of(context) == TextDirection.rtl;
|
|
|
|
return Scaffold(
|
|
backgroundColor: const Color(0xFFF0F2F5),
|
|
appBar: AppBar(
|
|
title: Text(
|
|
'لوحة الإحصائيات'.tr,
|
|
style: const TextStyle(
|
|
color: Color(0xFF1A1A1A),
|
|
fontWeight: FontWeight.w800,
|
|
fontSize: 22,
|
|
),
|
|
),
|
|
backgroundColor: Colors.transparent,
|
|
elevation: 0,
|
|
centerTitle: true,
|
|
iconTheme: const IconThemeData(color: Colors.black87),
|
|
leading: IconButton(
|
|
onPressed: () => Get.back(),
|
|
icon: Icon(
|
|
isRtl ? Icons.arrow_forward_ios : Icons.arrow_back_ios,
|
|
size: 20,
|
|
)),
|
|
actions: [
|
|
Container(
|
|
margin: const EdgeInsets.symmetric(horizontal: 10),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.grey.withOpacity(0.1),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 4))
|
|
],
|
|
),
|
|
child: IconButton(
|
|
onPressed: () async {
|
|
await controller.getAll();
|
|
},
|
|
icon: const Icon(Icons.refresh_rounded,
|
|
color: AppColor.primaryColor),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
body: GetBuilder<StaticController>(
|
|
builder: (staticController) {
|
|
if (staticController.isLoading) {
|
|
return const Center(child: MyCircularProgressIndicator());
|
|
}
|
|
|
|
return AnimationLimiter(
|
|
child: ListView(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
|
physics: const BouncingScrollPhysics(),
|
|
children: AnimationConfiguration.toStaggeredList(
|
|
duration: const Duration(milliseconds: 500),
|
|
childAnimationBuilder: (widget) => SlideAnimation(
|
|
verticalOffset: 50.0,
|
|
child: FadeInAnimation(child: widget),
|
|
),
|
|
children: [
|
|
Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(bottom: 10),
|
|
child: Text(
|
|
_formatDateRange(staticController),
|
|
style: TextStyle(
|
|
color: Colors.grey[600],
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 14),
|
|
),
|
|
),
|
|
),
|
|
|
|
_buildSectionHeader("نظرة عامة على النمو", Icons.trending_up),
|
|
|
|
// --- Passengers Card ---
|
|
_buildModernChartCard(
|
|
context,
|
|
title: 'الركاب',
|
|
totalValue:
|
|
staticController.totalMonthlyPassengers.toString(),
|
|
spots: staticController.chartDataPassengers
|
|
.cast<FlSpot>()
|
|
.toList(),
|
|
compareSpots: staticController.isComparing
|
|
? staticController.chartDataPassengersCompare
|
|
.cast<FlSpot>()
|
|
.toList()
|
|
: null,
|
|
baseColor: const Color(0xFF2979FF),
|
|
icon: Icons.groups_rounded,
|
|
controller: staticController,
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// --- Drivers Card ---
|
|
_buildModernChartCard(
|
|
context,
|
|
title: 'السائقون',
|
|
totalValue: staticController.totalMonthlyDrivers.toString(),
|
|
spots: staticController.chartDataDrivers
|
|
.cast<FlSpot>()
|
|
.toList(),
|
|
compareSpots: staticController.isComparing
|
|
? staticController.chartDataDriversCompare
|
|
.cast<FlSpot>()
|
|
.toList()
|
|
: null,
|
|
baseColor: const Color(0xFFFF9100),
|
|
icon: Icons.drive_eta_rounded,
|
|
controller: staticController,
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// --- Rides Card ---
|
|
_buildModernChartCard(
|
|
context,
|
|
title: 'الرحلات',
|
|
totalValue: staticController.totalMonthlyRides.toString(),
|
|
spots:
|
|
staticController.chartDataRides.cast<FlSpot>().toList(),
|
|
compareSpots: staticController.isComparing
|
|
? staticController.chartDataRidesCompare
|
|
.cast<FlSpot>()
|
|
.toList()
|
|
: null,
|
|
baseColor: const Color(0xFF651FFF),
|
|
icon: Icons.map_rounded,
|
|
controller: staticController,
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
_buildSectionHeader(
|
|
"أداء فريق العمل", Icons.workspaces_filled),
|
|
|
|
// --- Activations Card (No Click Action) ---
|
|
_buildMultiLineChartCard(
|
|
context: context,
|
|
title: 'تفعيل السائقين (Activations)',
|
|
spotsrama1: staticController.chartDataEmployeerama1,
|
|
spotsShahd: staticController.chartDataEmployeeshahd,
|
|
spotsRama: staticController.chartDataEmployeeRama2,
|
|
spotsMayar: staticController.chartDataEmployeeSefer4,
|
|
spotsrama1Compare:
|
|
staticController.chartDataEmployeerama1Compare,
|
|
spotsShahdCompare:
|
|
staticController.chartDataEmployeeshahdCompare,
|
|
spotsRamaCompare:
|
|
staticController.chartDataEmployeeRama2Compare,
|
|
spotsMayarCompare:
|
|
staticController.chartDataEmployeeSefer4Compare,
|
|
staticController: staticController,
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// --- 🔴 Modified: Calls Card (With Navigation) ---
|
|
_buildMultiLineChartCard(
|
|
context: context,
|
|
title: 'عدد المكالمات (Calls)',
|
|
// إضافة التوجيه للصفحة الجديدة عند الضغط
|
|
onTap: () {
|
|
Get.to(() => const DailyNotesView());
|
|
},
|
|
spotsrama1: staticController.chartDataCallsrama1,
|
|
spotsShahd: staticController.chartDataCallsShahd,
|
|
spotsRama: staticController.chartDataCallsRama2,
|
|
spotsMayar: staticController.chartDataCallsSefer4,
|
|
spotsrama1Compare:
|
|
staticController.chartDataCallsrama1Compare,
|
|
spotsShahdCompare:
|
|
staticController.chartDataCallsShahdCompare,
|
|
spotsRamaCompare:
|
|
staticController.chartDataCallsRama2Compare,
|
|
spotsMayarCompare:
|
|
staticController.chartDataCallsSefer4Compare,
|
|
staticController: staticController,
|
|
),
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
// --- Employment Stats List ---
|
|
if (staticController.employmentStatsList.isNotEmpty) ...[
|
|
_buildSectionHeader(
|
|
"إجمالي الإدخالات حسب الموظف", Icons.list_alt_rounded),
|
|
_buildEmploymentStatsList(
|
|
staticController.employmentStatsList),
|
|
const SizedBox(height: 24),
|
|
],
|
|
|
|
_buildSectionHeader(
|
|
"متابعة التسجيل", Icons.assignment_turned_in),
|
|
|
|
// --- Drivers Matching Notes Card ---
|
|
_buildModernChartCard(
|
|
context,
|
|
title: 'سائقين بعد الاتصال',
|
|
totalValue: staticController.staticList.isNotEmpty &&
|
|
staticController.staticList[0]
|
|
['totalMonthlyMatchingNotes'] !=
|
|
null
|
|
? "${staticController.staticList[0]['totalMonthlyMatchingNotes']}"
|
|
: "0",
|
|
spots: staticController.chartDataDriversMatchingNotes
|
|
.cast<FlSpot>()
|
|
.toList(),
|
|
compareSpots: staticController.isComparing
|
|
? staticController.chartDataDriversMatchingNotesCompare
|
|
.cast<FlSpot>()
|
|
.toList()
|
|
: null,
|
|
baseColor: const Color(0xFF00BFA5),
|
|
icon: Icons.phone_in_talk_rounded,
|
|
controller: staticController,
|
|
),
|
|
const SizedBox(height: 40),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
// ... (Employment Stats List - No Change)
|
|
Widget _buildEmploymentStatsList(List<Map<String, dynamic>> stats) {
|
|
Color getEmployeeColor(String name) {
|
|
String n = name.toLowerCase().trim();
|
|
if (n.contains('shahd')) return Colors.redAccent;
|
|
if (n.contains('mayar')) return Colors.amber.shade700;
|
|
if (n.contains('rama2')) return Colors.green;
|
|
if (n.contains('rama1')) return Colors.blue;
|
|
return Colors.blueGrey;
|
|
}
|
|
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(20),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.04),
|
|
blurRadius: 15,
|
|
offset: const Offset(0, 5))
|
|
]),
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(Icons.people_outline, color: Colors.blueGrey.shade700),
|
|
const SizedBox(width: 10),
|
|
Text("الأداء الإجمالي (العدد)",
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 16,
|
|
color: Colors.grey.shade800)),
|
|
],
|
|
),
|
|
const Divider(height: 25),
|
|
Wrap(
|
|
spacing: 12,
|
|
runSpacing: 12,
|
|
children: stats.map((item) {
|
|
Color itemColor = getEmployeeColor(item['name'].toString());
|
|
return Container(
|
|
width: 100,
|
|
padding:
|
|
const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
|
|
decoration: BoxDecoration(
|
|
color: itemColor.withOpacity(0.08),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(
|
|
color: itemColor.withOpacity(0.5), width: 1.5)),
|
|
child: Column(
|
|
children: [
|
|
Text(
|
|
item['name'].toString().toUpperCase(),
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.bold,
|
|
color: itemColor),
|
|
textAlign: TextAlign.center,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
item['count'].toString(),
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w900,
|
|
color: Colors.grey.shade800),
|
|
)
|
|
],
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// ... (Header Widget - No Change)
|
|
Widget _buildSectionHeader(String title, IconData icon) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 12.0, top: 8.0, right: 4),
|
|
child: Row(
|
|
children: [
|
|
Icon(icon, size: 20, color: Colors.grey[600]),
|
|
const SizedBox(width: 8),
|
|
Text(title,
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.grey[800],
|
|
fontFamily: 'Cairo')),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// ... (Modern Chart Card - No Change)
|
|
Widget _buildModernChartCard(BuildContext context,
|
|
{required String title,
|
|
required String totalValue,
|
|
required List<FlSpot> spots,
|
|
List<FlSpot>? compareSpots,
|
|
required Color baseColor,
|
|
required IconData icon,
|
|
required StaticController controller}) {
|
|
List<FlSpot> allSpots = [...spots];
|
|
if (compareSpots != null) allSpots.addAll(compareSpots);
|
|
double maxY = _calculateMaxY(allSpots);
|
|
double interval = _calculateInterval(maxY);
|
|
double daysInPeriod = _getDaysInPeriod(controller);
|
|
double xInterval = _calculateXInterval(daysInPeriod);
|
|
|
|
return Container(
|
|
height: 400,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(24),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: baseColor.withOpacity(0.08),
|
|
blurRadius: 20,
|
|
offset: const Offset(0, 8))
|
|
]),
|
|
child: Column(
|
|
children: [
|
|
_buildCardHeader(context, title, totalValue, icon, baseColor,
|
|
controller.isComparing),
|
|
Expanded(
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 10),
|
|
child: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: LineChart(LineChartData(
|
|
minX: 1,
|
|
maxX: daysInPeriod > 0 ? daysInPeriod : 1,
|
|
minY: 0,
|
|
maxY: maxY,
|
|
lineTouchData: _buildTooltipData(controller),
|
|
gridData: _buildGridData(interval),
|
|
titlesData: _buildTitlesData(
|
|
interval, xInterval, daysInPeriod, controller),
|
|
borderData: FlBorderData(show: false),
|
|
lineBarsData: [
|
|
if (compareSpots != null && compareSpots.isNotEmpty)
|
|
_buildLine(compareSpots, Colors.grey.withOpacity(0.4),
|
|
isDashed: true, isStep: true),
|
|
_buildLine(spots, baseColor, isStep: true),
|
|
])),
|
|
),
|
|
),
|
|
),
|
|
_buildControlBar(context, controller, baseColor),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// --- 🔴 Modified: Multi Line Chart (Adding onTap) ---
|
|
Widget _buildMultiLineChartCard({
|
|
required BuildContext context,
|
|
required String title,
|
|
required StaticController staticController,
|
|
// Add onTap parameter
|
|
VoidCallback? onTap,
|
|
required List<FlSpot> spotsrama1,
|
|
required List<FlSpot> spotsShahd,
|
|
required List<FlSpot> spotsRama,
|
|
required List<FlSpot> spotsMayar,
|
|
required List<FlSpot> spotsrama1Compare,
|
|
required List<FlSpot> spotsShahdCompare,
|
|
required List<FlSpot> spotsRamaCompare,
|
|
required List<FlSpot> spotsMayarCompare,
|
|
}) {
|
|
final allSpots = [
|
|
...spotsrama1,
|
|
...spotsShahd,
|
|
...spotsRama,
|
|
...spotsMayar
|
|
];
|
|
double maxY = _calculateMaxY(allSpots);
|
|
double interval = _calculateInterval(maxY);
|
|
double daysInPeriod = _getDaysInPeriod(staticController);
|
|
double xInterval = _calculateXInterval(daysInPeriod);
|
|
|
|
return Container(
|
|
height: 460,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(24),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 15,
|
|
offset: const Offset(0, 5))
|
|
]),
|
|
child: Column(
|
|
children: [
|
|
// 🔴 Wrap Header with InkWell if onTap is provided
|
|
InkWell(
|
|
onTap: onTap,
|
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
|
child: _buildCardHeader(
|
|
context,
|
|
title,
|
|
null,
|
|
Icons.bar_chart_rounded,
|
|
Colors.black54,
|
|
staticController.isComparing,
|
|
showArrow: onTap != null), // Pass showArrow flag
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
|
child: Wrap(
|
|
spacing: 16,
|
|
runSpacing: 8,
|
|
alignment: WrapAlignment.center,
|
|
children: [
|
|
_buildLegendItem("راما 1", Colors.blue),
|
|
_buildLegendItem("شهد", Colors.redAccent),
|
|
_buildLegendItem("راما 2", Colors.green),
|
|
_buildLegendItem("ميار", Colors.amber.shade700),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 15),
|
|
Expanded(
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 10),
|
|
child: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: LineChart(LineChartData(
|
|
minX: 1,
|
|
maxX: daysInPeriod > 0 ? daysInPeriod : 1,
|
|
minY: 0,
|
|
maxY: maxY,
|
|
lineTouchData: _buildTooltipData(staticController),
|
|
gridData: _buildGridData(interval),
|
|
titlesData: _buildTitlesData(
|
|
interval, xInterval, daysInPeriod, staticController),
|
|
borderData: FlBorderData(show: false),
|
|
lineBarsData: [
|
|
if (staticController.isComparing) ...[
|
|
_buildLine(
|
|
spotsrama1Compare, Colors.blue.withOpacity(0.3),
|
|
isDashed: true, isStep: false),
|
|
_buildLine(spotsShahdCompare,
|
|
Colors.redAccent.withOpacity(0.3),
|
|
isDashed: true, isStep: false),
|
|
_buildLine(
|
|
spotsRamaCompare, Colors.green.withOpacity(0.3),
|
|
isDashed: true, isStep: false),
|
|
_buildLine(spotsMayarCompare,
|
|
Colors.amber.shade700.withOpacity(0.3),
|
|
isDashed: true, isStep: false),
|
|
],
|
|
_buildLine(spotsrama1, Colors.blue, isStep: false),
|
|
_buildLine(spotsShahd, Colors.redAccent, isStep: false),
|
|
_buildLine(spotsRama, Colors.green, isStep: false),
|
|
_buildLine(spotsMayar, Colors.amber.shade700,
|
|
isStep: false),
|
|
])),
|
|
),
|
|
),
|
|
),
|
|
_buildControlBar(context, staticController, Colors.blueGrey),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// ... (Shared Components) ...
|
|
// 🔴 Modified _buildCardHeader to accept showArrow
|
|
Widget _buildCardHeader(BuildContext context, String title,
|
|
String? totalValue, IconData icon, Color color, bool isComparing,
|
|
{bool showArrow = false}) {
|
|
return Padding(
|
|
padding: const EdgeInsets.all(20.0),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: color.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(16)),
|
|
child: Icon(icon, color: color, size: 26)),
|
|
const SizedBox(width: 14),
|
|
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
|
Text(title,
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.grey[800])),
|
|
if (totalValue != null) ...[
|
|
const SizedBox(height: 4),
|
|
Text('الإجمالي: $totalValue',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
color: Colors.grey[500],
|
|
fontWeight: FontWeight.w600))
|
|
],
|
|
// إضافة نص صغير يدل على إمكانية الضغط
|
|
if (showArrow)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 4.0),
|
|
child: Text("اضغط لعرض التفاصيل اليومية",
|
|
style: TextStyle(
|
|
fontSize: 10, color: AppColor.primaryColor)),
|
|
)
|
|
]),
|
|
],
|
|
),
|
|
Row(children: [
|
|
if (showArrow)
|
|
Icon(Icons.arrow_forward_ios_rounded,
|
|
size: 16, color: Colors.grey[400]),
|
|
if (isComparing) ...[
|
|
const SizedBox(width: 8),
|
|
Container(
|
|
width: 8,
|
|
height: 8,
|
|
decoration: const BoxDecoration(
|
|
color: Colors.grey, shape: BoxShape.circle)),
|
|
const SizedBox(width: 4),
|
|
Text("الفترة السابقة",
|
|
style: TextStyle(fontSize: 10, color: Colors.grey[600]))
|
|
]
|
|
])
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
LineTouchData _buildTooltipData(StaticController controller) {
|
|
return LineTouchData(
|
|
handleBuiltInTouches: true,
|
|
touchTooltipData: LineTouchTooltipData(
|
|
getTooltipColor: (touchedSpot) => Colors.black87,
|
|
tooltipPadding: const EdgeInsets.all(10),
|
|
tooltipRoundedRadius: 8,
|
|
getTooltipItems: (List<LineBarSpot> touchedBarSpots) {
|
|
return touchedBarSpots.map((barSpot) {
|
|
DateTime? start = controller.startDate;
|
|
if (start != null) {
|
|
DateTime date = start.add(Duration(days: barSpot.x.toInt() - 1));
|
|
String formattedDate = DateFormat('d MMM', 'en').format(date);
|
|
return LineTooltipItem(
|
|
'$formattedDate \n',
|
|
const TextStyle(
|
|
color: Colors.white70,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 12),
|
|
children: [
|
|
TextSpan(
|
|
text: barSpot.y.toInt().toString(),
|
|
style: TextStyle(
|
|
color: barSpot.bar.color,
|
|
fontWeight: FontWeight.w900,
|
|
fontSize: 14))
|
|
]);
|
|
}
|
|
return LineTooltipItem(
|
|
barSpot.y.toString(),
|
|
const TextStyle(
|
|
color: Colors.white, fontWeight: FontWeight.bold));
|
|
}).toList();
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
FlGridData _buildGridData(double interval) {
|
|
return FlGridData(
|
|
show: true,
|
|
drawVerticalLine: false,
|
|
horizontalInterval: interval,
|
|
getDrawingHorizontalLine: (value) =>
|
|
FlLine(color: Colors.grey.withOpacity(0.08), strokeWidth: 1));
|
|
}
|
|
|
|
FlTitlesData _buildTitlesData(double interval, double xInterval,
|
|
double daysInPeriod, StaticController controller) {
|
|
return FlTitlesData(
|
|
show: true,
|
|
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
|
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
|
bottomTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
reservedSize: 30,
|
|
interval: xInterval,
|
|
getTitlesWidget: (value, meta) {
|
|
if (value <= 0 || value > daysInPeriod)
|
|
return const SizedBox.shrink();
|
|
if (controller.startDate != null) {
|
|
int dayOffset = value.toInt() - 1;
|
|
DateTime date =
|
|
controller.startDate!.add(Duration(days: dayOffset));
|
|
String text = DateFormat('d/M', 'en').format(date);
|
|
return Padding(
|
|
padding: const EdgeInsets.only(top: 8.0),
|
|
child: Text(text,
|
|
style: TextStyle(
|
|
color: Colors.grey[400],
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.bold)));
|
|
}
|
|
return const SizedBox.shrink();
|
|
})),
|
|
leftTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
interval: interval,
|
|
reservedSize: 40,
|
|
getTitlesWidget: (value, meta) => Text(_formatNumber(value),
|
|
style: TextStyle(color: Colors.grey[400], fontSize: 10)))),
|
|
);
|
|
}
|
|
|
|
Widget _buildControlBar(
|
|
BuildContext context, StaticController controller, Color color) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[50],
|
|
borderRadius:
|
|
const BorderRadius.vertical(bottom: Radius.circular(24)),
|
|
border: Border(top: BorderSide(color: Colors.grey.shade200))),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: InkWell(
|
|
onTap: () async {
|
|
DateTimeRange? picked = await showDateRangePicker(
|
|
context: context,
|
|
firstDate: DateTime(2020),
|
|
lastDate: DateTime.now().add(const Duration(days: 1)),
|
|
initialDateRange: DateTimeRange(
|
|
start: controller.startDate ??
|
|
DateTime.now()
|
|
.subtract(const Duration(days: 30)),
|
|
end: controller.endDate ?? DateTime.now()),
|
|
builder: (context, child) {
|
|
return Theme(
|
|
data: Theme.of(context).copyWith(
|
|
colorScheme: ColorScheme.light(
|
|
primary: AppColor.primaryColor,
|
|
onPrimary: Colors.white,
|
|
onSurface: Colors.black)),
|
|
child: child!);
|
|
});
|
|
if (picked != null)
|
|
controller.updateDateRange(picked.start, picked.end);
|
|
},
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
vertical: 8, horizontal: 12),
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: Colors.grey.shade300),
|
|
borderRadius: BorderRadius.circular(8),
|
|
color: Colors.white),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.calendar_today_rounded,
|
|
size: 16, color: color),
|
|
const SizedBox(width: 8),
|
|
Text(_formatDateRange(controller),
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.grey[800]),
|
|
overflow: TextOverflow.ellipsis)
|
|
])))),
|
|
const SizedBox(width: 12),
|
|
TextButton.icon(
|
|
onPressed: controller.toggleComparison,
|
|
style: TextButton.styleFrom(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8))),
|
|
icon: Icon(
|
|
controller.isComparing
|
|
? Icons.visibility_off_outlined
|
|
: Icons.compare_arrows_rounded,
|
|
size: 18,
|
|
color: controller.isComparing ? Colors.redAccent : color),
|
|
label: Text(controller.isComparing ? "إلغاء" : "مقارنة",
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: controller.isComparing ? Colors.redAccent : color,
|
|
fontWeight: FontWeight.w600)))
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
LineChartBarData _buildLine(List<FlSpot> spots, Color color,
|
|
{bool isDashed = false, bool isStep = false}) {
|
|
return LineChartBarData(
|
|
spots: spots,
|
|
isCurved: !isStep, // Curved if not step
|
|
curveSmoothness: 0.25,
|
|
isStepLineChart: isStep,
|
|
color: color,
|
|
barWidth: isDashed ? 2 : 2.5,
|
|
isStrokeCapRound: !isStep,
|
|
dotData: const FlDotData(show: false),
|
|
dashArray: isDashed ? [5, 5] : null,
|
|
lineChartStepData: const LineChartStepData(stepDirection: 0.5),
|
|
);
|
|
}
|
|
|
|
Widget _buildLegendItem(String name, Color color) {
|
|
return Row(mainAxisSize: MainAxisSize.min, children: [
|
|
Container(
|
|
width: 10,
|
|
height: 10,
|
|
decoration: BoxDecoration(color: color, shape: BoxShape.circle)),
|
|
const SizedBox(width: 6),
|
|
Text(name,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey[700],
|
|
fontWeight: FontWeight.w600))
|
|
]);
|
|
}
|
|
|
|
double _getDaysInPeriod(StaticController controller) {
|
|
if (controller.startDate == null || controller.endDate == null) return 30;
|
|
return controller.endDate!.difference(controller.startDate!).inDays + 1.0;
|
|
}
|
|
|
|
double _calculateXInterval(double daysInPeriod) {
|
|
if (daysInPeriod > 60) return 30;
|
|
if (daysInPeriod > 30) return 10;
|
|
if (daysInPeriod > 10) return 5;
|
|
return 1;
|
|
}
|
|
|
|
String _formatDateRange(StaticController controller) {
|
|
if (controller.startDate == null || controller.endDate == null)
|
|
return controller.currentDateString;
|
|
String start =
|
|
"${controller.startDate!.day}/${controller.startDate!.month}/${controller.startDate!.year}";
|
|
String end =
|
|
"${controller.endDate!.day}/${controller.endDate!.month}/${controller.endDate!.year}";
|
|
return "$start - $end";
|
|
}
|
|
|
|
double _calculateMaxY(List<FlSpot> spots) {
|
|
if (spots.isEmpty) return 10;
|
|
double maxVal = 0;
|
|
for (var spot in spots) {
|
|
if (spot.y > maxVal) maxVal = spot.y;
|
|
}
|
|
if (maxVal == 0) return 5;
|
|
return (maxVal * 1.2).ceilToDouble();
|
|
}
|
|
|
|
double _calculateInterval(double maxY) {
|
|
if (maxY <= 10) return 2;
|
|
if (maxY <= 50) return 10;
|
|
if (maxY <= 100) return 20;
|
|
if (maxY <= 500) return 100;
|
|
if (maxY <= 1000) return 200;
|
|
return maxY / 5;
|
|
}
|
|
|
|
String _formatNumber(double value) {
|
|
if (value >= 1000) return '${(value / 1000).toStringAsFixed(1)}k';
|
|
return value.toInt().toString();
|
|
}
|
|
}
|