Files
Siro/siro_admin/lib/views/admin/admin_home_page.dart
T

1691 lines
64 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:siro_admin/constant/theme.dart';
import 'dart:math';
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:siro_admin/views/admin/drivers/driver_gift_check_page.dart';
import 'package:siro_admin/views/admin/drivers/driver_tracker_screen.dart';
import 'package:siro_admin/views/admin/quality/blacklist_page.dart';
import '../../constant/box_name.dart';
import '../../controller/admin/dashboard_controller.dart';
import '../../controller/admin/static_controller.dart';
import '../../controller/functions/crud.dart';
import '../../controller/notification_controller.dart';
import '../../main.dart';
import '../../print.dart';
import '../widgets/snackbar.dart';
import '../invoice/invoice_list_page.dart';
import 'captain/captain.dart';
import 'captain/syrian_driver_not_active.dart';
import 'drivers/monitor_ride.dart';
import 'employee/employee_page.dart';
import 'enceypt/encrypt.dart';
import 'error/error/error_page.dart';
import 'packages.dart';
import 'passenger/passenger.dart';
import 'rides/ride_lookup_page.dart';
import 'server/monitor_server_page.dart';
import 'static/static.dart';
import 'wallet/wallet.dart';
import 'staff/add_staff_page.dart';
import 'staff/pending_admins_page.dart';
import 'dashboard_v2_widget.dart';
import 'static/advanced_analytics_page.dart';
import 'financial/financial_v2_page.dart';
import 'security/audit_logs_page.dart';
import 'analytics/live_analytics_page.dart';
import 'package:siro_admin/views/widgets/responsive_layout.dart';
import 'package:siro_admin/views/widgets/web_sidebar.dart';
import '../transit/org_list_page.dart';
import 'package:siro_admin/views/widgets/glass_container.dart';
class AdminHomePage extends StatefulWidget {
const AdminHomePage({super.key});
@override
State<AdminHomePage> createState() => _AdminHomePageState();
}
class _AdminHomePageState extends State<AdminHomePage>
with SingleTickerProviderStateMixin {
final TextEditingController _messageController = TextEditingController();
final TextEditingController _searchController = TextEditingController();
late AnimationController _pulseController;
late bool isSuperAdmin;
late DashboardController dashboardController;
String _searchQuery = '';
@override
void initState() {
super.initState();
_pulseController = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat(reverse: true);
final String role = box.read('admin_role')?.toString() ?? 'admin';
final String myPhone = box.read(BoxName.adminPhone)?.toString() ?? '';
isSuperAdmin = (role == 'super_admin') ||
(myPhone == '201023248456' ||
myPhone == '963992952235' ||
myPhone == '963942542053');
Log.print('AdminHomePage: role=$role, isSuperAdmin=$isSuperAdmin');
dashboardController = Get.put(DashboardController());
}
@override
void dispose() {
_pulseController.dispose();
_messageController.dispose();
_searchController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return ResponsiveLayout(
mobile: _buildMobileScaffold(cs),
desktop: Scaffold(
backgroundColor: cs.surface,
body: Row(
children: [
const WebSidebar(selectedIndex: 0),
Expanded(child: _buildDesktopScaffold(cs)),
],
),
),
);
}
Widget _buildMobileScaffold(ColorScheme cs) {
return Scaffold(
backgroundColor: cs.surface,
body: RefreshIndicator(
onRefresh: () async => await dashboardController.getDashBoard(),
color: cs.primary,
backgroundColor: cs.surfaceContainerHighest,
child: GetBuilder<DashboardController>(
builder: (controller) {
if (controller.dashbord.isEmpty) {
return _buildLoadingState(cs);
}
final data = controller.dashbord[0];
final categories = _getFilteredCategories();
return CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: [
_buildSliverAppBar(controller, cs),
_buildSearchBar(cs),
if (_searchQuery.isEmpty) const DashboardV2Widget(),
if (_searchQuery.isEmpty)
_buildQuickStatsSection(data, controller, cs),
SliverPadding(
padding: const EdgeInsets.only(bottom: 60),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final category = categories[index];
if (category.items.isEmpty) {
return const SizedBox.shrink();
}
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 450),
child: SlideAnimation(
verticalOffset: 40.0,
child: FadeInAnimation(
child: _buildCategorySection(category, cs),
),
),
);
},
childCount: categories.length,
),
),
),
],
);
},
),
),
);
}
Widget _buildDesktopScaffold(ColorScheme cs) {
return Scaffold(
backgroundColor: cs.surface,
body: GetBuilder<DashboardController>(
builder: (controller) {
if (controller.dashbord.isEmpty) {
return _buildLoadingState(cs);
}
final data = controller.dashbord[0];
return CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: [
_buildSliverAppBar(controller, cs),
const DashboardV2Widget(),
_buildQuickStatsSection(data, controller, cs),
],
);
},
),
);
}
Widget _buildLoadingState(ColorScheme cs) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [cs.primary.withValues(alpha: 0.3), Colors.transparent],
),
),
child: Center(
child: SizedBox(
width: 28,
height: 28,
child: CircularProgressIndicator(
color: cs.primary,
strokeWidth: 2.5,
),
),
),
),
const SizedBox(height: 16),
Text('جاري التحميل...',
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13)),
],
),
);
}
Widget _buildSliverAppBar(DashboardController controller, ColorScheme cs) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return SliverAppBar(
expandedHeight: 130.0,
floating: true,
pinned: true,
backgroundColor: cs.surface,
elevation: 0,
flexibleSpace: FlexibleSpaceBar(
collapseMode: CollapseMode.pin,
background: Stack(
fit: StackFit.expand,
children: [
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isDark
? [
cs.primary.withValues(alpha: 0.08),
cs.surface,
cs.tertiary.withValues(alpha: 0.05),
]
: [
cs.primary.withValues(alpha: 0.05),
cs.surface,
cs.tertiary.withValues(alpha: 0.03),
],
),
),
),
Positioned(
top: -30,
left: -40,
child: _GlowOrb(color: cs.primary, size: 150, opacity: 0.08),
),
Positioned(
top: -20,
right: -20,
child: _GlowOrb(color: cs.tertiary, size: 120, opacity: 0.06),
),
Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.only(bottom: 18),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildLogo(cs),
const SizedBox(height: 6),
Text(
isSuperAdmin ? 'Super Admin Panel' : 'Admin Panel',
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 11,
letterSpacing: 1.5,
),
),
],
),
),
),
],
),
),
actions: [
_buildHeaderAction(
Icons.refresh_rounded, () => controller.getDashBoard(), cs),
const SizedBox(width: 8),
],
);
}
Widget _buildLogo(ColorScheme cs) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedBuilder(
animation: _pulseController,
builder: (_, __) {
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
gradient: LinearGradient(
colors: [
cs.primary.withValues(
alpha: 0.3 + 0.1 * _pulseController.value),
cs.primary.withValues(alpha: 0.1),
],
),
border: Border.all(
color: cs.primary.withValues(
alpha: 0.4 + 0.2 * _pulseController.value),
width: 1,
),
),
child: Icon(
Icons.admin_panel_settings_rounded,
color: cs.primary,
size: 18,
),
);
},
),
const SizedBox(width: 10),
ShaderMask(
shaderCallback: (bounds) => LinearGradient(
colors: [cs.primary, cs.tertiary],
).createShader(bounds),
child: const Text(
'Siro Admin',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w700,
fontSize: 20,
letterSpacing: 0.5,
),
),
),
],
);
}
Widget _buildHeaderAction(IconData icon, VoidCallback onTap, ColorScheme cs) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: cs.outline),
),
child: Icon(icon, color: cs.onSurfaceVariant, size: 18),
),
);
}
Widget _buildSearchBar(ColorScheme cs) {
final isFocused = _searchQuery.isNotEmpty;
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
child: Container(
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: isFocused ? cs.primary.withValues(alpha: 0.5) : cs.outline,
width: isFocused ? 1.5 : 1,
),
),
child: TextField(
controller: _searchController,
onChanged: (val) => setState(() => _searchQuery = val),
style: TextStyle(color: cs.onSurface, fontSize: 14),
decoration: InputDecoration(
hintText: 'ابحث عن خدمة أو ميزة...',
hintStyle: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
prefixIcon:
Icon(Icons.search_rounded, color: cs.primary, size: 20),
suffixIcon: _searchQuery.isNotEmpty
? IconButton(
icon: Icon(Icons.close_rounded,
color: cs.onSurfaceVariant, size: 18),
onPressed: () {
setState(() {
_searchQuery = '';
_searchController.clear();
});
},
)
: null,
border: InputBorder.none,
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
),
),
),
),
);
}
Widget _buildQuickStatsSection(
dynamic data, DashboardController controller, ColorScheme cs) {
final highlights = [
_HighlightData(
'إجمالي الركاب', data['countPassengers'], Icons.group_rounded, cs.tertiary),
_HighlightData('إجمالي السائقين', data['countDriver'],
Icons.drive_eta_rounded, cs.secondary),
_HighlightData('رحلات الشهر', data['countRideThisMonth'],
Icons.calendar_today_rounded, cs.primary),
if (isSuperAdmin)
_HighlightData('المحفظة', _formatCurrency(data['seferWallet']),
Icons.account_balance_wallet_rounded, cs.primary),
];
final detailedStats = _getDetailedStats(data, controller, cs);
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(top: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 10),
child: Row(
children: [
Container(
width: 3,
height: 14,
decoration: BoxDecoration(
color: cs.primary,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 8),
Text('نظرة عامة',
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 11,
fontWeight: FontWeight.w600,
letterSpacing: 1.2,
)),
],
),
),
SizedBox(
height: 108,
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
itemCount: highlights.length,
itemBuilder: (ctx, i) => Padding(
padding: EdgeInsets.only(
right: i < highlights.length - 1 ? 10 : 0),
child: _buildHighlightCard(highlights[i], cs),
),
),
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Container(
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: cs.outline),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: List.generate(detailedStats.length, (i) {
final stat = detailedStats[i];
return Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildDetailStatItem(stat, cs),
if (i < detailedStats.length - 1)
Container(
width: 1,
height: 36,
color: cs.outline,
),
],
);
}),
),
),
),
),
),
_buildChartsSection(data, controller, cs),
const SizedBox(height: 8),
],
),
),
);
}
/// توزيع الرحلات على فترات اليوم (صباح / مساء / ليل).
/// الحدود مطابقة لما يحسبه backend/Admin/dashbord.php:
/// صباحاً 6–11، مساءً 12–17، ليلاً 18–5.
Widget _buildPeakHoursCard(
double morning, double evening, double night, ColorScheme cs) {
final total = morning + evening + night;
final maxVal = [morning, evening, night].reduce((a, b) => a > b ? a : b);
final maxY = maxVal > 0 ? (maxVal * 1.3).ceilToDouble() : 10.0;
final slots = [
(label: 'صباحاً', hint: '6–11', value: morning, color: cs.warning),
(label: 'مساءً', hint: '12–17', value: evening, color: cs.info),
(label: 'ليلاً', hint: '18–5', value: night, color: cs.secondary),
];
return AnimationConfiguration.staggeredList(
position: 1,
duration: const Duration(milliseconds: 450),
child: SlideAnimation(
verticalOffset: 40.0,
child: FadeInAnimation(
child: Container(
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: cs.outline),
),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.schedule_rounded, color: cs.primary, size: 18),
const SizedBox(width: 8),
Text('توزيع الرحلات حسب الوقت',
style: TextStyle(
color: cs.onSurface,
fontSize: 13,
fontWeight: FontWeight.w600)),
const Spacer(),
if (total > 0)
Text('${total.toInt()} رحلة',
style: TextStyle(
color: cs.onSurfaceVariant, fontSize: 11)),
],
),
const SizedBox(height: 16),
SizedBox(
height: 140,
child: Directionality(
textDirection: TextDirection.ltr,
child: BarChart(
BarChartData(
alignment: BarChartAlignment.spaceAround,
maxY: maxY,
barTouchData: BarTouchData(
touchTooltipData: BarTouchTooltipData(
getTooltipColor: (_) => cs.surfaceContainerHighest,
getTooltipItem: (group, groupIndex, rod, rodIndex) {
final pct = total > 0
? (rod.toY / total * 100).toStringAsFixed(1)
: '0.0';
return BarTooltipItem(
'${rod.toY.toInt()} ($pct%)',
TextStyle(
color: cs.onSurface,
fontSize: 11,
fontWeight: FontWeight.w600),
);
},
),
),
gridData: FlGridData(
show: true,
drawVerticalLine: false,
getDrawingHorizontalLine: (v) => FlLine(
color: cs.outline.withValues(alpha: 0.5),
strokeWidth: 1),
),
borderData: FlBorderData(show: false),
titlesData: FlTitlesData(
show: true,
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false)),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false)),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 34,
getTitlesWidget: (v, _) => Text(
v >= 1000
? '${(v / 1000).toStringAsFixed(0)}k'
: v.toInt().toString(),
style: TextStyle(
color: cs.onSurfaceVariant, fontSize: 9),
),
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 34,
getTitlesWidget: (value, meta) {
final i = value.toInt();
if (i < 0 || i >= slots.length) {
return const SizedBox();
}
return Padding(
padding: const EdgeInsets.only(top: 6),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(slots[i].label,
style: TextStyle(
color: cs.onSurface,
fontSize: 10,
fontWeight: FontWeight.w600)),
Text(slots[i].hint,
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 8)),
],
),
);
},
),
),
),
barGroups: [
for (var i = 0; i < slots.length; i++)
BarChartGroupData(
x: i,
barRods: [
BarChartRodData(
toY: slots[i].value,
color: slots[i].color,
width: 26,
borderRadius: BorderRadius.circular(6),
backDrawRodData: BackgroundBarChartRodData(
show: true,
toY: maxY,
color: cs.outline.withValues(alpha: 0.25),
),
),
],
),
],
),
),
),
),
],
),
),
),
),
);
}
Widget _buildChartsSection(
dynamic data, DashboardController controller, ColorScheme cs) {
final completed = (data['completed_rides'] ?? 0).toDouble();
final cancelled = (data['cancelled_rides'] ?? 0).toDouble();
final total = completed + cancelled;
final completedPercent = total > 0 ? (completed / total * 100).toStringAsFixed(1) : '0.0';
final comfort = (data['comfort'] ?? 0).toDouble();
final speed = (data['speed'] ?? 0).toDouble();
final lady = (data['lady'] ?? 0).toDouble();
final maxBar = [comfort, speed, lady].reduce((a, b) => a > b ? a : b);
final barMaxY = maxBar > 0 ? (maxBar * 1.3).ceilToDouble() : 10.0;
final payments = (data['payments'] ?? 0).toDouble();
final passengerCount = (data['countPassengers'] ?? 0).toDouble();
final driverCount = (data['countDriver'] ?? 0).toDouble();
final monthRides = (data['countRideThisMonth'] ?? 0).toDouble();
// توزيع الرحلات على أوقات اليوم — تعيده dashbord.php منذ البداية
// (morning/evening/night_ride_count) ولم يكن معروضاً في أي شاشة.
final morningRides = (data['morning_ride_count'] ?? 0).toDouble();
final eveningRides = (data['evening_ride_count'] ?? 0).toDouble();
final nightRides = (data['night_ride_count'] ?? 0).toDouble();
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(4, 0, 4, 10),
child: Row(
children: [
Container(
width: 3,
height: 14,
decoration: BoxDecoration(
color: cs.primary,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 8),
Text('تحليل البيانات',
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 11,
fontWeight: FontWeight.w600,
letterSpacing: 1.2,
)),
],
),
),
Container(
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: cs.outline),
),
padding: const EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: SizedBox(
height: 200,
child: Stack(
alignment: Alignment.center,
children: [
PieChart(
PieChartData(
sectionsSpace: 2,
centerSpaceRadius: 35,
sections: [
PieChartSectionData(
value: completed,
color: cs.primary,
radius: 55,
title: '',
),
PieChartSectionData(
value: cancelled,
color: cs.error,
radius: 55,
title: '',
),
],
),
),
Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'$completedPercent%',
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Text(
total.toInt().toString(),
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 10,
),
),
],
),
],
),
),
),
const SizedBox(width: 8),
Expanded(
child: SizedBox(
height: 200,
child: BarChart(
BarChartData(
alignment: BarChartAlignment.spaceAround,
maxY: barMaxY,
barTouchData: BarTouchData(
touchTooltipData: BarTouchTooltipData(
getTooltipColor: (_) => cs.surfaceContainerHighest,
getTooltipItem: (group, groupIndex, rod, rodIndex) {
return BarTooltipItem(
rod.toY.toInt().toString(),
TextStyle(
color: cs.onSurface,
fontSize: 11,
fontWeight: FontWeight.w600,
),
);
},
),
),
titlesData: FlTitlesData(
show: true,
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
leftTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
getTitlesWidget: (value, meta) {
final labels = ['Comfort', 'Speed', 'Lady'];
if (value.toInt() < labels.length) {
return Padding(
padding: const EdgeInsets.only(top: 6),
child: Text(
labels[value.toInt()],
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 10,
),
),
);
}
return const SizedBox.shrink();
},
),
),
),
gridData: const FlGridData(show: false),
borderData: FlBorderData(show: false),
barGroups: [
BarChartGroupData(
x: 0,
barRods: [
BarChartRodData(
toY: comfort,
color: cs.tertiary,
width: 28,
borderRadius: const BorderRadius.vertical(top: Radius.circular(6)),
),
],
),
BarChartGroupData(
x: 1,
barRods: [
BarChartRodData(
toY: speed,
color: cs.warning,
width: 28,
borderRadius: const BorderRadius.vertical(top: Radius.circular(6)),
),
],
),
BarChartGroupData(
x: 2,
barRods: [
BarChartRodData(
toY: lady,
color: const Color(0xFFF48FB1),
width: 28,
borderRadius: const BorderRadius.vertical(top: Radius.circular(6)),
),
],
),
],
),
),
),
),
],
),
),
const SizedBox(height: 12),
AnimationConfiguration.staggeredList(
position: 0,
duration: const Duration(milliseconds: 450),
child: SlideAnimation(
verticalOffset: 40.0,
child: FadeInAnimation(
child: Container(
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: cs.outline),
),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.account_balance_rounded, color: cs.primary, size: 18),
const SizedBox(width: 8),
Text('نظرة عامة على الإيرادات', style: TextStyle(color: cs.onSurface, fontSize: 13, fontWeight: FontWeight.w600)),
],
),
const SizedBox(height: 16),
SizedBox(
height: 120,
child: BarChart(
BarChartData(
alignment: BarChartAlignment.center,
maxY: (payments * 1.3) > 0 ? (payments * 1.3) : 10,
barTouchData: BarTouchData(
touchTooltipData: BarTouchTooltipData(
getTooltipColor: (_) => cs.surfaceContainerHighest,
getTooltipItem: (group, groupIndex, rod, rodIndex) {
return BarTooltipItem(
_formatCurrency(rod.toY),
TextStyle(color: cs.onSurface, fontSize: 11, fontWeight: FontWeight.w600),
);
},
),
),
titlesData: FlTitlesData(
show: true,
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
leftTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
getTitlesWidget: (value, meta) {
return Padding(
padding: const EdgeInsets.only(top: 6),
child: Text('Payments', style: TextStyle(color: cs.onSurfaceVariant, fontSize: 10)),
);
},
),
),
),
gridData: const FlGridData(show: false),
borderData: FlBorderData(show: false),
barGroups: [
BarChartGroupData(
x: 0,
barRods: [
BarChartRodData(
toY: payments,
color: cs.primary,
width: 40,
borderRadius: const BorderRadius.vertical(top: Radius.circular(6)),
),
],
),
],
),
),
),
const SizedBox(height: 12),
Center(
child: Text(
'إجمالي المدفوعات: ${_formatCurrency(payments)}',
style: TextStyle(color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.bold),
),
),
],
),
),
),
),
),
const SizedBox(height: 12),
AnimationConfiguration.staggeredList(
position: 1,
duration: const Duration(milliseconds: 450),
child: SlideAnimation(
verticalOffset: 40.0,
child: FadeInAnimation(
child: Container(
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: cs.outline),
),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.compare_arrows_rounded, color: cs.primary, size: 18),
const SizedBox(width: 8),
Text('نسبة السائقين إلى الركاب', style: TextStyle(color: cs.onSurface, fontSize: 13, fontWeight: FontWeight.w600)),
],
),
const SizedBox(height: 16),
_buildRatioBar('السائقين', driverCount, driverCount + passengerCount, cs.secondary, cs),
const SizedBox(height: 12),
_buildRatioBar('الركاب', passengerCount, driverCount + passengerCount, cs.tertiary, cs),
],
),
),
),
),
),
const SizedBox(height: 12),
_buildPeakHoursCard(morningRides, eveningRides, nightRides, cs),
const SizedBox(height: 12),
AnimationConfiguration.staggeredList(
position: 2,
duration: const Duration(milliseconds: 450),
child: SlideAnimation(
verticalOffset: 40.0,
child: FadeInAnimation(
child: Container(
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: cs.outline),
),
padding: const EdgeInsets.all(16),
child: Row(
children: [
_buildMiniStatCard('رحلات الشهر', monthRides, Icons.calendar_today_rounded, cs.primary, cs),
_buildMiniStatCard('الركاب', passengerCount, Icons.group_rounded, cs.tertiary, cs),
_buildMiniStatCard('السائقين', driverCount, Icons.drive_eta_rounded, cs.secondary, cs),
],
),
),
),
),
),
],
),
);
}
Widget _buildRatioBar(String label, double value, double total, Color color, ColorScheme cs) {
final ratio = total > 0 ? value / total : 0.0;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: TextStyle(color: cs.onSurfaceVariant, fontSize: 11)),
Text(value.toInt().toString(), style: TextStyle(color: cs.onSurface, fontSize: 12, fontWeight: FontWeight.w600)),
],
),
const SizedBox(height: 6),
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: LinearProgressIndicator(
value: ratio,
backgroundColor: cs.surface,
color: color,
minHeight: 12,
),
),
],
);
}
Widget _buildMiniStatCard(String label, dynamic value, IconData icon, Color color, ColorScheme cs) {
return Expanded(
child: Column(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: color, size: 22),
),
const SizedBox(height: 8),
Text(value.toString(), style: TextStyle(color: cs.onSurface, fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 2),
Text(label, style: TextStyle(color: cs.onSurfaceVariant, fontSize: 10)),
],
),
);
}
Widget _buildHighlightCard(_HighlightData h, ColorScheme cs) {
return GlassContainer(
width: 155,
padding: const EdgeInsets.all(16),
borderRadius: 16,
gradientColors: [
h.color.withValues(alpha: 0.18),
h.color.withValues(alpha: 0.03),
],
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
padding: const EdgeInsets.all(7),
decoration: BoxDecoration(
color: h.color.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(9),
),
child: Icon(h.icon, color: h.color, size: 16),
),
Container(
width: 6,
height: 6,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: h.color.withValues(alpha: 0.6),
),
),
],
),
const Spacer(),
Text(
h.value.toString(),
style: TextStyle(
color: cs.onSurface,
fontSize: 20,
fontWeight: FontWeight.w700,
height: 1.1,
),
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 3),
Text(
h.label,
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 10,
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
),
],
),
);
}
Widget _buildDetailStatItem(Map<String, dynamic> stat, ColorScheme cs) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(stat['icon'] as IconData,
color: stat['color'] as Color, size: 20),
const SizedBox(height: 6),
Text(
stat['value'].toString(),
style: TextStyle(
color: cs.onSurface,
fontSize: 14,
fontWeight: FontWeight.bold,
height: 1,
),
),
const SizedBox(height: 3),
Text(
stat['title'] as String,
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 9,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
Widget _buildCategorySection(ActionCategory category, ColorScheme cs) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 12),
child: Row(
children: [
Container(
width: 3,
height: 14,
decoration: BoxDecoration(
color: cs.primary,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 8),
Text(
category.title,
style: TextStyle(
color: cs.onSurface,
fontSize: 15,
fontWeight: FontWeight.w600,
letterSpacing: 0.3,
),
),
const SizedBox(width: 10),
Expanded(
child: Container(height: 1, color: cs.outline),
),
const SizedBox(width: 8),
Text(
'${category.items.length}',
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 11,
fontWeight: FontWeight.w500,
),
),
],
),
),
GridView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 120,
childAspectRatio: 0.88,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
itemCount: category.items.length,
itemBuilder: (context, index) =>
_buildActionItem(category.items[index], cs),
),
const SizedBox(height: 8),
],
);
}
Widget _buildActionItem(ActionItem item, ColorScheme cs) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: item.onPressed,
borderRadius: BorderRadius.circular(16),
splashColor: item.color.withValues(alpha: 0.1),
highlightColor: item.color.withValues(alpha: 0.05),
child: Container(
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: cs.outline),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(11),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
item.color.withValues(alpha: 0.20),
item.color.withValues(alpha: 0.08),
],
),
borderRadius: BorderRadius.circular(12),
border:
Border.all(color: item.color.withValues(alpha: 0.25)),
boxShadow: [
BoxShadow(
color: item.color.withValues(alpha: 0.15),
blurRadius: 10,
offset: const Offset(0, 3),
),
],
),
child: Icon(item.icon, color: item.color, size: 22),
),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6),
child: Text(
item.title,
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: cs.onSurface,
fontSize: 11,
fontWeight: FontWeight.w500,
height: 1.3,
),
),
),
],
),
),
),
);
}
List<ActionCategory> _getFilteredCategories() {
final all = _getAllActionCategories();
if (_searchQuery.isEmpty) return all;
return all
.map((cat) {
final matched = cat.items
.where((item) =>
item.title.toLowerCase().contains(_searchQuery.toLowerCase()))
.toList();
return matched.isEmpty
? null
: ActionCategory(title: cat.title, items: matched);
})
.whereType<ActionCategory>()
.toList();
}
List<ActionCategory> _getAllActionCategories() {
final cs = Theme.of(context).colorScheme;
return [
ActionCategory(
title: 'المستخدمين',
items: [
ActionItem('الركاب', Icons.people_outline_rounded, cs.info,
() => Get.to(() => Passengrs())),
ActionItem('السائقون', Icons.drive_eta_rounded, cs.warning,
() => Get.to(() => CaptainsPage())),
ActionItem('المراقب', Icons.track_changes_rounded, cs.danger,
() => Get.to(() => SiroTrackerScreen())),
],
),
ActionCategory(
title: 'مواصلاتي',
items: [
ActionItem('المؤسسات', Icons.directions_bus_filled_rounded, const Color(0xFF6366F1),
() => Get.to(() => const TransitOrgListPage())),
],
),
ActionCategory(
title: 'إدارة النظام الجديد',
items: [
ActionItem('أكواد الخصم', Icons.confirmation_number_rounded, const Color(0xFF6366F1),
() => Get.toNamed('/promo')),
ActionItem('تعديل الأسعار', Icons.settings_suggest_rounded, cs.warning,
() => Get.toNamed('/kazan')),
ActionItem('الشكاوى', Icons.report_problem_rounded, cs.danger,
() => Get.toNamed('/complaints')),
ActionItem('مراجعة الوثائق', Icons.assignment_ind_rounded, cs.info,
() => Get.toNamed('/driver-docs')),
ActionItem('استخبارات السوشيال', Icons.insights_rounded, const Color(0xFFC792EA),
() => Get.toNamed('/social-intelligence')),
],
),
ActionCategory(
title: 'العمليات',
items: [
ActionItem('الرحلات', Icons.map_rounded, const Color(0xFF82AAFF),
() => Get.to(() => RidesDashboardScreen())),
if (isSuperAdmin)
ActionItem(
'مراقبة الرحلات',
Icons.remove_red_eye_rounded,
const Color(0xFFC792EA),
() => Get.to(() => RideMonitorScreen())),
ActionItem('الإحصائيات', Icons.bar_chart_rounded, const Color(0xFF6366F1), () async {
await Get.put(StaticController()).getAll();
Get.to(() => const StaticDash());
}),
ActionItem('التحليلات المتقدمة', Icons.analytics_rounded, cs.info,
() => Get.to(() => const AdvancedAnalyticsPage())),
ActionItem('لوحة البيانات التفاعلية', Icons.dashboard_customize_rounded,
const Color(0xFF00CEC9),
() => Get.to(() => const LiveAnalyticsPage())),
],
),
ActionCategory(
title: 'الجودة والدعم',
items: [
ActionItem('القائمة السوداء', Icons.block_flipped, cs.danger,
() => Get.to(() => const BlacklistPage())),
],
),
if (true)
ActionCategory(
title: 'المالية والإدارة',
items: [
ActionItem('الإدارة المالية V2', Icons.account_balance_rounded,
const Color(0xFF6366F1), () => Get.to(() => const FinancialV2Page())),
ActionItem('المحفظة', Icons.account_balance_wallet_rounded, const Color(0xFF6366F1),
() => Get.to(() => Wallet())),
ActionItem('هدية 300', Icons.card_giftcard_rounded, cs.warning,
() => Get.to(() => DriverGiftCheckPage())),
ActionItem('الفواتير', Icons.receipt_long_rounded,
cs.success, () => Get.to(() => InvoiceListPage())),
ActionItem('الموظفون', Icons.badge_rounded, const Color(0xFF9CA3AF),
() => Get.to(() => EmployeePage())),
ActionItem('موافقة المشرفين', Icons.how_to_reg_rounded, const Color(0xFF6366F1),
() => Get.to(() => const PendingAdminsPage())),
],
),
if (true)
ActionCategory(
title: 'النظام والتواصل',
items: [
ActionItem('سجل العمليات', Icons.admin_panel_settings_rounded,
cs.danger, () => Get.to(() => const AuditLogsPage())),
ActionItem('واتساب جماعي', Icons.message_rounded,
const Color(0xFF25D366), () => _showWhatsAppDialog(context)),
ActionItem('أتمتة التسويق', Icons.campaign_rounded,
cs.success, () => Get.toNamed('/marketing')),
ActionItem(
'إشعار سائقين',
Icons.notifications_active_rounded,
const Color(0xFFFF7043),
() => Get.put(NotificationController())
.sendNotificationDrivers()),
ActionItem(
'إشعار ركاب',
Icons.notification_important_rounded,
const Color(0xFFF06292),
() => Get.put(NotificationController())
.sendNotificationPassengers()),
ActionItem('تسجيل سائق', Icons.person_add_rounded, cs.info,
() => Get.to(() => DriversPendingPage())),
ActionItem(
'تحديث التطبيق',
Icons.system_update_rounded,
const Color(0xFF9CA3AF),
() => Get.to(() => PackageUpdateScreen())),
ActionItem('مراقب السيرفر', Icons.dns_rounded, const Color(0xFF6366F1),
() => Get.to(() => ServerMonitorPage())),
ActionItem('سجل الأخطاء', Icons.error_outline_rounded, cs.danger,
() => Get.to(() => ErrorListPage())),
ActionItem(
'أداة التشفير',
Icons.lock_rounded,
const Color(0xFF9575CD),
() => Get.to(() => EncryptToolPage(
adminToken: box.read(BoxName.adminPhone),
))),
],
),
if (isSuperAdmin)
ActionCategory(
title: 'إدارة الكوادر',
items: [
ActionItem(
'إضافة مدير',
Icons.admin_panel_settings_rounded,
const Color(0xFF6366F1),
() => Get.to(() => const AddStaffPage(role: 'admin')),
),
ActionItem(
'إضافة خدمة عملاء',
Icons.support_agent_rounded,
cs.info,
() => Get.to(() => const AddStaffPage(role: 'service')),
),
],
),
];
}
List<Map<String, dynamic>> _getDetailedStats(
dynamic data, DashboardController controller, ColorScheme cs) {
return [
if (isSuperAdmin)
{
'title': 'رصيد الرسائل',
'value': controller.creditSMS,
'icon': Icons.sms_rounded,
'color': cs.tertiary,
},
{
'title': 'مكتملة',
'value': data['completed_rides'],
'icon': Icons.check_circle_rounded,
'color': cs.success,
},
{
'title': 'ملغاة',
'value': data['cancelled_rides'],
'icon': Icons.cancel_rounded,
'color': cs.danger,
},
{
'title': 'مدفوعات',
'value': _formatCurrency(data['payments']),
'icon': Icons.attach_money_rounded,
'color': cs.warning,
},
{
'title': 'Comfort',
'value': data['comfort'],
'icon': Icons.chair_rounded,
'color': cs.success,
},
{
'title': 'Speed',
'value': data['speed'],
'icon': Icons.flash_on_rounded,
'color': const Color(0xFFFBBF24),
},
{
'title': 'Lady',
'value': data['lady'],
'icon': Icons.woman_rounded,
'color': const Color(0xFFF48FB1),
},
];
}
void _showWhatsAppDialog(BuildContext context) {
final cs = Theme.of(context).colorScheme;
Get.dialog(
Dialog(
backgroundColor: Colors.transparent,
child: Container(
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(24),
border: Border.all(color: cs.outline),
boxShadow: [
BoxShadow(
color: cs.shadow.withValues(alpha: 0.3),
blurRadius: 30,
offset: const Offset(0, 12),
),
],
),
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF25D366).withValues(alpha: 0.12),
shape: BoxShape.circle,
border: Border.all(
color: const Color(0xFF25D366).withValues(alpha: 0.25)),
),
child: const Icon(Icons.message_rounded,
color: Color(0xFF25D366), size: 28),
),
const SizedBox(height: 16),
Text(
'إرسال واتساب جماعي',
style: TextStyle(
color: cs.onSurface,
fontSize: 17,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 4),
Text(
'سيتم إرسال الرسالة لجميع السائقين',
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 11),
),
const SizedBox(height: 20),
Container(
decoration: BoxDecoration(
color: cs.surface,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: cs.outline),
),
child: TextField(
controller: _messageController,
maxLines: 4,
style: TextStyle(color: cs.onSurface, fontSize: 13),
decoration: InputDecoration(
hintText: 'اكتب رسالتك هنا...',
hintStyle: TextStyle(color: cs.onSurfaceVariant, fontSize: 12),
border: InputBorder.none,
contentPadding: const EdgeInsets.all(14),
),
),
),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: TextButton(
onPressed: () => Get.back(),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: cs.outline),
),
),
child: Text(
'إلغاء',
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
icon: const Icon(Icons.send_rounded, size: 16),
label:
const Text('إرسال', style: TextStyle(fontSize: 13)),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF25D366),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 0,
),
onPressed: () async {
if (_messageController.text.isNotEmpty) {
Get.back();
var driverPhones = box
.read(BoxName.tokensDrivers)['message'] as List?;
if (driverPhones == null || driverPhones.isEmpty) {
return;
}
mySnackbarInfo('سيتم الإرسال في الخلفية');
for (var driverData in driverPhones) {
if (driverData['phone'] != null) {
await CRUD().sendWhatsAppAuth(
driverData['phone'].toString(),
_messageController.text);
await Future.delayed(
Duration(seconds: Random().nextInt(3) + 1));
}
}
_messageController.clear();
}
},
),
),
],
),
],
),
),
),
);
}
String _formatCurrency(dynamic value) {
if (value == null) return '0.0';
return double.tryParse(value.toString())?.toStringAsFixed(1) ?? '0.0';
}
}
class _GlowOrb extends StatelessWidget {
final Color color;
final double size;
final double opacity;
const _GlowOrb(
{required this.color, required this.size, required this.opacity});
@override
Widget build(BuildContext context) {
return Container(
width: size,
height: size,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [color.withValues(alpha: opacity), Colors.transparent],
),
),
);
}
}
class _HighlightData {
final String label;
final dynamic value;
final IconData icon;
final Color color;
_HighlightData(this.label, this.value, this.icon, this.color);
}
class ActionItem {
final String title;
final IconData icon;
final Color color;
final VoidCallback onPressed;
ActionItem(this.title, this.icon, this.color, this.onPressed);
}
class ActionCategory {
final String title;
final List<ActionItem> items;
ActionCategory({required this.title, required this.items});
}