1124 lines
41 KiB
Dart
1124 lines
41 KiB
Dart
import 'dart:math';
|
|
import 'dart:ui';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:sefer_admin1/views/admin/drivers/driver_gift_check_page.dart';
|
|
import 'package:sefer_admin1/views/admin/drivers/driver_tracker_screen.dart';
|
|
import 'package:sefer_admin1/views/admin/quality/blacklist_page.dart';
|
|
|
|
import '../../constant/box_name.dart';
|
|
import '../../constant/colors.dart';
|
|
import '../../constant/style.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 '../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/driver_fingerprint_migration.dart';
|
|
import 'enceypt/encrypt.dart';
|
|
import 'enceypt/fingerprint_migration.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';
|
|
|
|
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 = '';
|
|
|
|
// ══════════════════ DESIGN TOKENS ══════════════════
|
|
// --- Unified with AppColor ---
|
|
static const Color _bg = AppColor.bg;
|
|
static const Color _surface = AppColor.surface;
|
|
static const Color _surfaceElevated = AppColor.surfaceElevated;
|
|
static const Color _accent = AppColor.accent;
|
|
static const Color _accentSoft = AppColor.accentSoft;
|
|
static const Color _accentBorder = AppColor.accentBorder;
|
|
static const Color _danger = AppColor.danger;
|
|
static const Color _warning = AppColor.warning;
|
|
static const Color _info = AppColor.info;
|
|
static const Color _success = AppColor.success;
|
|
static const Color _textPrimary = AppColor.textPrimary;
|
|
static const Color _textSecondary = AppColor.textSecondary;
|
|
static const Color _divider = AppColor.divider;
|
|
|
|
@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();
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// BUILD
|
|
// ══════════════════════════════════════════════════════════════
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: _bg,
|
|
body: RefreshIndicator(
|
|
onRefresh: () async => await dashboardController.getDashBoard(),
|
|
color: _accent,
|
|
backgroundColor: _surface,
|
|
child: GetBuilder<DashboardController>(
|
|
builder: (controller) {
|
|
if (controller.dashbord.isEmpty) {
|
|
return _buildLoadingState();
|
|
}
|
|
|
|
final data = controller.dashbord[0];
|
|
final categories = _getFilteredCategories();
|
|
|
|
return CustomScrollView(
|
|
physics: const BouncingScrollPhysics(),
|
|
slivers: [
|
|
_buildSliverAppBar(controller),
|
|
_buildSearchBar(),
|
|
if (_searchQuery.isEmpty) const DashboardV2Widget(),
|
|
if (_searchQuery.isEmpty)
|
|
_buildQuickStatsSection(data, controller),
|
|
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),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
childCount: categories.length,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// LOADING STATE
|
|
// ══════════════════════════════════════════════════════════════
|
|
Widget _buildLoadingState() {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 60,
|
|
height: 60,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
gradient: RadialGradient(
|
|
colors: [_accent.withOpacity(0.3), Colors.transparent],
|
|
),
|
|
),
|
|
child: const Center(
|
|
child: SizedBox(
|
|
width: 28,
|
|
height: 28,
|
|
child: CircularProgressIndicator(
|
|
color: _accent,
|
|
strokeWidth: 2.5,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text('جاري التحميل...',
|
|
style: TextStyle(color: _textSecondary, fontSize: 13)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// SLIVER APP BAR
|
|
// ══════════════════════════════════════════════════════════════
|
|
Widget _buildSliverAppBar(DashboardController controller) {
|
|
return SliverAppBar(
|
|
expandedHeight: 130.0,
|
|
floating: true,
|
|
pinned: true,
|
|
backgroundColor: _bg,
|
|
elevation: 0,
|
|
flexibleSpace: FlexibleSpaceBar(
|
|
collapseMode: CollapseMode.pin,
|
|
background: Stack(
|
|
fit: StackFit.expand,
|
|
children: [
|
|
// Aurora gradient background
|
|
Container(
|
|
decoration: const BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: [
|
|
Color(0xFF0D2137),
|
|
Color(0xFF0D1117),
|
|
Color(0xFF0F1F1A),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
// Subtle glow orbs
|
|
Positioned(
|
|
top: -30,
|
|
left: -40,
|
|
child: _GlowOrb(color: _accent, size: 150, opacity: 0.08),
|
|
),
|
|
Positioned(
|
|
top: -20,
|
|
right: -20,
|
|
child: _GlowOrb(color: _info, size: 120, opacity: 0.06),
|
|
),
|
|
// Content
|
|
Align(
|
|
alignment: Alignment.bottomCenter,
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(bottom: 18),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
_buildLogo(),
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
isSuperAdmin ? 'Super Admin Panel' : 'Admin Panel',
|
|
style: TextStyle(
|
|
color: _textSecondary,
|
|
fontSize: 11,
|
|
letterSpacing: 1.5,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
actions: [
|
|
_buildHeaderAction(
|
|
Icons.refresh_rounded, () => controller.getDashBoard()),
|
|
const SizedBox(width: 8),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildLogo() {
|
|
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: [
|
|
_accent.withOpacity(0.3 + 0.1 * _pulseController.value),
|
|
_accent.withOpacity(0.1),
|
|
],
|
|
),
|
|
border: Border.all(
|
|
color:
|
|
_accent.withOpacity(0.4 + 0.2 * _pulseController.value),
|
|
width: 1,
|
|
),
|
|
),
|
|
child: const Icon(
|
|
Icons.admin_panel_settings_rounded,
|
|
color: _accent,
|
|
size: 18,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
const SizedBox(width: 10),
|
|
ShaderMask(
|
|
shaderCallback: (bounds) => const LinearGradient(
|
|
colors: [_accent, _info],
|
|
).createShader(bounds),
|
|
child: const Text(
|
|
'Intaleq Admin',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.w700,
|
|
fontSize: 20,
|
|
letterSpacing: 0.5,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildHeaderAction(IconData icon, VoidCallback onTap) {
|
|
return GestureDetector(
|
|
onTap: onTap,
|
|
child: Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: _surface,
|
|
borderRadius: BorderRadius.circular(10),
|
|
border: Border.all(color: _divider),
|
|
),
|
|
child: Icon(icon, color: _textSecondary, size: 18),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// SEARCH BAR
|
|
// ══════════════════════════════════════════════════════════════
|
|
Widget _buildSearchBar() {
|
|
return SliverToBoxAdapter(
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: _surface,
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(
|
|
color: _searchQuery.isNotEmpty ? _accentBorder : _divider,
|
|
width: _searchQuery.isNotEmpty ? 1.5 : 1,
|
|
),
|
|
),
|
|
child: TextField(
|
|
controller: _searchController,
|
|
onChanged: (val) => setState(() => _searchQuery = val),
|
|
style: const TextStyle(color: _textPrimary, fontSize: 14),
|
|
decoration: InputDecoration(
|
|
hintText: 'ابحث عن خدمة أو ميزة...',
|
|
hintStyle: const TextStyle(color: _textSecondary, fontSize: 13),
|
|
prefixIcon:
|
|
const Icon(Icons.search_rounded, color: _accent, size: 20),
|
|
suffixIcon: _searchQuery.isNotEmpty
|
|
? IconButton(
|
|
icon: Icon(Icons.close_rounded,
|
|
color: _textSecondary, size: 18),
|
|
onPressed: () {
|
|
setState(() {
|
|
_searchQuery = '';
|
|
_searchController.clear();
|
|
});
|
|
},
|
|
)
|
|
: null,
|
|
border: InputBorder.none,
|
|
contentPadding:
|
|
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// QUICK STATS SECTION
|
|
// ══════════════════════════════════════════════════════════════
|
|
Widget _buildQuickStatsSection(dynamic data, DashboardController controller) {
|
|
final highlights = [
|
|
_HighlightData(
|
|
'إجمالي الركاب', data['countPassengers'], Icons.group_rounded, _info),
|
|
_HighlightData('إجمالي السائقين', data['countDriver'],
|
|
Icons.drive_eta_rounded, _warning),
|
|
_HighlightData('رحلات الشهر', data['countRideThisMonth'],
|
|
Icons.calendar_today_rounded, const Color(0xFFC792EA)),
|
|
if (isSuperAdmin)
|
|
_HighlightData('المحفظة', _formatCurrency(data['seferWallet']),
|
|
Icons.account_balance_wallet_rounded, _accent),
|
|
];
|
|
|
|
final detailedStats = _getDetailedStats(data, controller);
|
|
|
|
return SliverToBoxAdapter(
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(top: 8),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Section label
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(20, 8, 20, 10),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 3,
|
|
height: 14,
|
|
decoration: BoxDecoration(
|
|
color: _accent,
|
|
borderRadius: BorderRadius.circular(2),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
const Text('نظرة عامة',
|
|
style: TextStyle(
|
|
color: _textSecondary,
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: 1.2,
|
|
)),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Highlight Cards
|
|
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]),
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// Detailed stats strip
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: _surface,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: _divider),
|
|
),
|
|
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),
|
|
if (i < detailedStats.length - 1)
|
|
Container(
|
|
width: 1,
|
|
height: 36,
|
|
color: _divider,
|
|
),
|
|
],
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 8),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildHighlightCard(_HighlightData h) {
|
|
return Container(
|
|
width: 148,
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: _surface,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: h.color.withOpacity(0.2)),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: h.color.withOpacity(0.08),
|
|
blurRadius: 16,
|
|
offset: const Offset(0, 6),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(7),
|
|
decoration: BoxDecoration(
|
|
color: h.color.withOpacity(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.withOpacity(0.6),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const Spacer(),
|
|
Text(
|
|
h.value.toString(),
|
|
style: const TextStyle(
|
|
color: _textPrimary,
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.w700,
|
|
height: 1.1,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
const SizedBox(height: 3),
|
|
Text(
|
|
h.label,
|
|
style: const TextStyle(
|
|
color: _textSecondary,
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDetailStatItem(Map<String, dynamic> stat) {
|
|
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: const TextStyle(
|
|
color: _textPrimary,
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.bold,
|
|
height: 1,
|
|
),
|
|
),
|
|
const SizedBox(height: 3),
|
|
Text(
|
|
stat['title'] as String,
|
|
style: const TextStyle(
|
|
color: _textSecondary,
|
|
fontSize: 9,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// CATEGORY SECTION
|
|
// ══════════════════════════════════════════════════════════════
|
|
Widget _buildCategorySection(ActionCategory category) {
|
|
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: _accent,
|
|
borderRadius: BorderRadius.circular(2),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
category.title,
|
|
style: const TextStyle(
|
|
color: _textPrimary,
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: 0.3,
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: Container(height: 1, color: _divider),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'${category.items.length}',
|
|
style: const TextStyle(
|
|
color: _textSecondary,
|
|
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]),
|
|
),
|
|
const SizedBox(height: 8),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildActionItem(ActionItem item) {
|
|
return Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
onTap: item.onPressed,
|
|
borderRadius: BorderRadius.circular(16),
|
|
splashColor: item.color.withOpacity(0.1),
|
|
highlightColor: item.color.withOpacity(0.05),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: _surface,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: _divider),
|
|
),
|
|
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.withOpacity(0.20),
|
|
item.color.withOpacity(0.08),
|
|
],
|
|
),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: item.color.withOpacity(0.25)),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: item.color.withOpacity(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: const TextStyle(
|
|
color: _textPrimary,
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w500,
|
|
height: 1.3,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// DATA HELPERS
|
|
// ══════════════════════════════════════════════════════════════
|
|
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, _info,
|
|
() => Get.to(() => Passengrs())),
|
|
ActionItem('السائقون', Icons.drive_eta_rounded, _warning,
|
|
() => Get.to(() => CaptainsPage())),
|
|
ActionItem('المراقب', Icons.track_changes_rounded, _danger,
|
|
() => Get.to(() => IntaleqTrackerScreen())),
|
|
],
|
|
),
|
|
ActionCategory(
|
|
title: 'إدارة النظام الجديد',
|
|
items: [
|
|
ActionItem('أكواد الخصم', Icons.confirmation_number_rounded, _accent,
|
|
() => Get.toNamed('/promo')),
|
|
ActionItem('تعديل الأسعار', Icons.settings_suggest_rounded, _warning,
|
|
() => Get.toNamed('/kazan')),
|
|
ActionItem('الشكاوى', Icons.report_problem_rounded, _danger,
|
|
() => Get.toNamed('/complaints')),
|
|
ActionItem('مراجعة الوثائق', Icons.assignment_ind_rounded, _info,
|
|
() => Get.toNamed('/driver-docs')),
|
|
],
|
|
),
|
|
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, _accent, () async {
|
|
await Get.put(StaticController()).getAll();
|
|
Get.to(() => const StaticDash());
|
|
}),
|
|
ActionItem('التحليلات المتقدمة', Icons.analytics_rounded, _info,
|
|
() => Get.to(() => const AdvancedAnalyticsPage())),
|
|
],
|
|
),
|
|
ActionCategory(
|
|
title: 'الجودة والدعم',
|
|
items: [
|
|
ActionItem('القائمة السوداء', Icons.block_flipped, _danger,
|
|
() => Get.to(() => const BlacklistPage())),
|
|
],
|
|
),
|
|
if (true)
|
|
ActionCategory(
|
|
title: 'المالية والإدارة',
|
|
items: [
|
|
ActionItem('الإدارة المالية V2', Icons.account_balance_rounded, _accent,
|
|
() => Get.to(() => const FinancialV2Page())),
|
|
ActionItem('المحفظة', Icons.account_balance_wallet_rounded, _accent,
|
|
() => Get.to(() => Wallet())),
|
|
ActionItem('هدية 300', Icons.card_giftcard_rounded, _warning,
|
|
() => Get.to(() => DriverGiftCheckPage())),
|
|
ActionItem('الفواتير', Icons.receipt_long_rounded,
|
|
const Color(0xFF80CBC4), () => Get.to(() => InvoiceListPage())),
|
|
ActionItem('الموظفون', Icons.badge_rounded, const Color(0xFFB0BEC5),
|
|
() => Get.to(() => EmployeePage())),
|
|
ActionItem('موافقة المشرفين', Icons.how_to_reg_rounded, _accent,
|
|
() => Get.to(() => const PendingAdminsPage())),
|
|
],
|
|
),
|
|
if (true)
|
|
ActionCategory(
|
|
title: 'النظام والتواصل',
|
|
items: [
|
|
ActionItem('سجل العمليات', Icons.admin_panel_settings_rounded,
|
|
_danger, () => Get.to(() => const AuditLogsPage())),
|
|
ActionItem('واتساب جماعي', Icons.message_rounded,
|
|
const Color(0xFF4CAF50), () => _showWhatsAppDialog(context)),
|
|
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, _info,
|
|
() => Get.to(() => DriversPendingPage())),
|
|
ActionItem(
|
|
'تحديث التطبيق',
|
|
Icons.system_update_rounded,
|
|
const Color(0xFFA1887F),
|
|
() => Get.to(() => PackageUpdateScreen())),
|
|
ActionItem('مراقب السيرفر', Icons.dns_rounded, _accent,
|
|
() => Get.to(() => ServerMonitorPage())),
|
|
ActionItem('سجل الأخطاء', Icons.error_outline_rounded, _danger,
|
|
() => Get.to(() => ErrorListPage())),
|
|
ActionItem('encrypt fp', Icons.error_outline_rounded, _danger,
|
|
() => Get.to(() => FingerprintMigrationTool())),
|
|
ActionItem('encrypt fp drivers', Icons.error_outline_rounded,
|
|
_danger, () => Get.to(() => DriverFingerprintMigrationTool())),
|
|
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,
|
|
_accent,
|
|
() => Get.to(() => const AddStaffPage(role: 'admin')),
|
|
),
|
|
ActionItem(
|
|
'إضافة خدمة عملاء',
|
|
Icons.support_agent_rounded,
|
|
_info,
|
|
() => Get.to(() => const AddStaffPage(role: 'service')),
|
|
),
|
|
],
|
|
),
|
|
];
|
|
}
|
|
|
|
List<Map<String, dynamic>> _getDetailedStats(
|
|
dynamic data, DashboardController controller) {
|
|
return [
|
|
if (isSuperAdmin)
|
|
{
|
|
'title': 'رصيد الرسائل',
|
|
'value': controller.creditSMS,
|
|
'icon': Icons.sms_rounded,
|
|
'color': _info,
|
|
},
|
|
{
|
|
'title': 'مكتملة',
|
|
'value': data['completed_rides'],
|
|
'icon': Icons.check_circle_rounded,
|
|
'color': _success,
|
|
},
|
|
{
|
|
'title': 'ملغاة',
|
|
'value': data['cancelled_rides'],
|
|
'icon': Icons.cancel_rounded,
|
|
'color': _danger,
|
|
},
|
|
{
|
|
'title': 'مدفوعات',
|
|
'value': _formatCurrency(data['payments']),
|
|
'icon': Icons.attach_money_rounded,
|
|
'color': _warning,
|
|
},
|
|
{
|
|
'title': 'Comfort',
|
|
'value': data['comfort'],
|
|
'icon': Icons.chair_rounded,
|
|
'color': const Color(0xFF80CBC4),
|
|
},
|
|
{
|
|
'title': 'Speed',
|
|
'value': data['speed'],
|
|
'icon': Icons.flash_on_rounded,
|
|
'color': const Color(0xFFFFD54F),
|
|
},
|
|
{
|
|
'title': 'Lady',
|
|
'value': data['lady'],
|
|
'icon': Icons.woman_rounded,
|
|
'color': const Color(0xFFF48FB1),
|
|
},
|
|
];
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// WHATSAPP DIALOG
|
|
// ══════════════════════════════════════════════════════════════
|
|
void _showWhatsAppDialog(BuildContext context) {
|
|
Get.dialog(
|
|
Dialog(
|
|
backgroundColor: Colors.transparent,
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: _surfaceElevated,
|
|
borderRadius: BorderRadius.circular(24),
|
|
border: Border.all(color: _divider),
|
|
boxShadow: const [
|
|
BoxShadow(
|
|
color: Colors.black45,
|
|
blurRadius: 30,
|
|
offset: 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(0xFF4CAF50).withAlpha(30), // ~0.12 opacity
|
|
shape: BoxShape.circle,
|
|
border: Border.all(
|
|
color: const Color(0xFF4CAF50).withAlpha(64)), // ~0.25 opacity
|
|
),
|
|
child: const Icon(Icons.message_rounded,
|
|
color: Color(0xFF4CAF50), size: 28),
|
|
),
|
|
const SizedBox(height: 16),
|
|
const Text(
|
|
'إرسال واتساب جماعي',
|
|
style: TextStyle(
|
|
color: _textPrimary,
|
|
fontSize: 17,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
const Text(
|
|
'سيتم إرسال الرسالة لجميع السائقين',
|
|
style: TextStyle(color: _textSecondary, fontSize: 11),
|
|
),
|
|
const SizedBox(height: 20),
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: _bg,
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(color: _divider),
|
|
),
|
|
child: TextField(
|
|
controller: _messageController,
|
|
maxLines: 4,
|
|
style: const TextStyle(color: _textPrimary, fontSize: 13),
|
|
decoration: const InputDecoration(
|
|
hintText: 'اكتب رسالتك هنا...',
|
|
hintStyle: TextStyle(color: _textSecondary, fontSize: 12),
|
|
border: InputBorder.none,
|
|
contentPadding: 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: const BorderSide(color: _divider),
|
|
),
|
|
),
|
|
child: const Text(
|
|
'إلغاء',
|
|
style: TextStyle(color: _textSecondary, 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(0xFF4CAF50),
|
|
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;
|
|
|
|
Get.snackbar(
|
|
'بدأ الإرسال',
|
|
'سيتم الإرسال في الخلفية',
|
|
backgroundColor:
|
|
const Color(0xFF4CAF50).withOpacity(0.15),
|
|
colorText: _textPrimary,
|
|
borderRadius: 12,
|
|
margin: const EdgeInsets.all(16),
|
|
icon: const Icon(Icons.check_circle_rounded,
|
|
color: Color(0xFF4CAF50)),
|
|
);
|
|
|
|
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';
|
|
}
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// HELPER WIDGETS
|
|
// ══════════════════════════════════════════════════════════════
|
|
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.withOpacity(opacity), Colors.transparent],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// DATA CLASSES
|
|
// ══════════════════════════════════════════════════════════════
|
|
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});
|
|
}
|