1691 lines
64 KiB
Dart
1691 lines
64 KiB
Dart
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});
|
||
}
|