Files
intaleq_admin/lib/views/admin/static/static.dart
2026-01-20 23:39:59 +03:00

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();
}
}