1318 lines
47 KiB
Dart
1318 lines
47 KiB
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),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
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;
|
|
|
|
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: const Color(0xFFF59E0B),
|
|
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)),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
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() {
|
|
return [
|
|
ActionCategory(
|
|
title: 'المستخدمين',
|
|
items: [
|
|
ActionItem('الركاب', Icons.people_outline_rounded, const Color(0xFF3B82F6),
|
|
() => Get.to(() => Passengrs())),
|
|
ActionItem('السائقون', Icons.drive_eta_rounded, const Color(0xFFF59E0B),
|
|
() => Get.to(() => CaptainsPage())),
|
|
ActionItem('المراقب', Icons.track_changes_rounded, const Color(0xFFEF4444),
|
|
() => 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, const Color(0xFFF59E0B),
|
|
() => Get.toNamed('/kazan')),
|
|
ActionItem('الشكاوى', Icons.report_problem_rounded, const Color(0xFFEF4444),
|
|
() => Get.toNamed('/complaints')),
|
|
ActionItem('مراجعة الوثائق', Icons.assignment_ind_rounded, const Color(0xFF3B82F6),
|
|
() => 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, const Color(0xFF3B82F6),
|
|
() => Get.to(() => const AdvancedAnalyticsPage())),
|
|
ActionItem('لوحة البيانات التفاعلية', Icons.dashboard_customize_rounded,
|
|
const Color(0xFF00CEC9),
|
|
() => Get.to(() => const LiveAnalyticsPage())),
|
|
],
|
|
),
|
|
ActionCategory(
|
|
title: 'الجودة والدعم',
|
|
items: [
|
|
ActionItem('القائمة السوداء', Icons.block_flipped, const Color(0xFFEF4444),
|
|
() => 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, const Color(0xFFF59E0B),
|
|
() => Get.to(() => DriverGiftCheckPage())),
|
|
ActionItem('الفواتير', Icons.receipt_long_rounded,
|
|
const Color(0xFF10B981), () => 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,
|
|
const Color(0xFFEF4444), () => Get.to(() => const AuditLogsPage())),
|
|
ActionItem('واتساب جماعي', Icons.message_rounded,
|
|
const Color(0xFF25D366), () => _showWhatsAppDialog(context)),
|
|
ActionItem('أتمتة التسويق', Icons.campaign_rounded,
|
|
const Color(0xFF10B981), () => 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, const Color(0xFF3B82F6),
|
|
() => 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, const Color(0xFFEF4444),
|
|
() => 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,
|
|
const Color(0xFF3B82F6),
|
|
() => 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': const Color(0xFF10B981),
|
|
},
|
|
{
|
|
'title': 'ملغاة',
|
|
'value': data['cancelled_rides'],
|
|
'icon': Icons.cancel_rounded,
|
|
'color': const Color(0xFFEF4444),
|
|
},
|
|
{
|
|
'title': 'مدفوعات',
|
|
'value': _formatCurrency(data['payments']),
|
|
'icon': Icons.attach_money_rounded,
|
|
'color': const Color(0xFFF59E0B),
|
|
},
|
|
{
|
|
'title': 'Comfort',
|
|
'value': data['comfort'],
|
|
'icon': Icons.chair_rounded,
|
|
'color': const Color(0xFF10B981),
|
|
},
|
|
{
|
|
'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});
|
|
}
|