إصلاح توافق فلاتر مع Theme الجديد

This commit is contained in:
Hamza-Ayed
2026-07-26 11:33:20 +03:00
parent 7830efead7
commit 3af99cc18a
31 changed files with 4256 additions and 2833 deletions
+1 -1
View File
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"6c0baaebf70e0148f485f27d5616b3d3382da7
_flutter.loader.load({
serviceWorkerSettings: {
serviceWorkerVersion: "658515598" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
serviceWorkerVersion: "1676594277" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
}
});
@@ -8,7 +8,6 @@ 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 '../../constant/theme.dart';
import '../../controller/admin/dashboard_controller.dart';
import '../../controller/admin/static_controller.dart';
import '../../controller/functions/crud.dart';
+257 -197
View File
@@ -2,10 +2,8 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../constant/box_name.dart';
import '../../../main.dart';
import '../../widgets/my_scafold.dart';
import '../../widgets/mycircular.dart';
import '../../../controller/admin/captain_admin_controller.dart';
import '../../../main.dart';
import 'captain_details.dart';
class CaptainsPage extends StatelessWidget {
@@ -21,159 +19,84 @@ class CaptainsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return MyScafolld(
title: 'Search for Captain'.tr,
isleading: true,
body: [
Container(
height: MediaQuery.of(context).size.height,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Theme.of(context).primaryColor.withOpacity(0.03),
Colors.white,
],
return Scaffold(
backgroundColor: cs.surface,
body: Column(
children: [
_buildAppBar(context, cs),
_buildSearchSection(context, cs),
Expanded(
child: GetBuilder<CaptainAdminController>(
builder: (controller) {
if (controller.isLoading) {
return _buildLoadingState(cs);
}
final message = controller.captainData['message'];
if (message == null) {
return _buildEmptyState(cs);
}
final List<dynamic> captains =
message is List ? message : [message];
if (captains.isEmpty) {
return _buildEmptyState(cs);
}
return _buildResultsList(context, captains, cs);
},
),
),
child: Column(
children: [
_buildHeaderSection(context),
Expanded(
child: GetBuilder<CaptainAdminController>(
builder: (controller) {
if (controller.isLoading) {
return _buildLoadingState();
}
final message = controller.captainData['message'];
if (message == null) {
return _buildEmptyState();
}
// 🔥 الحل هنا: توحيد الشكل إلى List
final List<dynamic> captains =
message is List ? message : [message];
if (captains.isEmpty) {
return _buildEmptyState();
}
return _buildResultsList(context, captains);
},
),
),
],
),
),
],
);
}
// ================= HEADER =================
Widget _buildHeaderSection(BuildContext context) {
return Container(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 16),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
Icons.manage_search_rounded,
color: Theme.of(context).primaryColor,
size: 24,
),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Find Captain'.tr,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
Text(
'Search by phone number'.tr,
style: TextStyle(
fontSize: 13,
color: Colors.grey[600],
),
),
],
),
],
),
const SizedBox(height: 20),
_buildModernSearchBar(context),
],
),
);
}
Widget _buildModernSearchBar(BuildContext context) {
Widget _buildAppBar(BuildContext context, ColorScheme cs) {
return Container(
padding: const EdgeInsets.fromLTRB(16, 50, 16, 16),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey[200]!),
color: cs.surface,
border: Border(bottom: BorderSide(color: cs.outline)),
),
child: Row(
children: [
Expanded(
child: TextField(
controller: searchController,
keyboardType: TextInputType.phone,
style: const TextStyle(fontSize: 15),
decoration: InputDecoration(
hintText: '0990000000'.tr,
hintStyle: TextStyle(color: Colors.grey[400], fontSize: 14),
prefixIcon: Icon(Icons.phone_android_rounded,
color: Colors.grey[400], size: 22),
border: InputBorder.none,
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
GestureDetector(
onTap: () => Get.back(),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: cs.outline),
),
onSubmitted: (_) => _performSearch(),
child: Icon(Icons.arrow_back_ios_new_rounded,
color: cs.onSurfaceVariant, size: 16),
),
),
Padding(
padding: const EdgeInsets.all(6.0),
child: Material(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(12),
child: InkWell(
onTap: _performSearch,
borderRadius: BorderRadius.circular(12),
child: const Padding(
padding: EdgeInsets.all(12),
child: Icon(Icons.search, color: Colors.white, size: 24),
),
),
const SizedBox(width: 12),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: cs.primary.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: cs.primary.withValues(alpha: 0.25)),
),
child: Icon(Icons.drive_eta_rounded,
color: cs.primary, size: 18),
),
const SizedBox(width: 10),
Text(
'قائمة الكباتن',
style: TextStyle(
color: cs.onSurface,
fontSize: 18,
fontWeight: FontWeight.w700,
),
),
],
@@ -181,6 +104,58 @@ class CaptainsPage extends StatelessWidget {
);
}
Widget _buildSearchSection(BuildContext context, ColorScheme cs) {
return Container(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
child: Container(
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: cs.outline),
),
child: Row(
children: [
Expanded(
child: TextField(
controller: searchController,
keyboardType: TextInputType.phone,
style: TextStyle(color: cs.onSurface, fontSize: 14),
cursorColor: cs.primary,
decoration: InputDecoration(
hintText: '0990000000',
hintStyle: TextStyle(
color: cs.onSurfaceVariant, fontSize: 13),
prefixIcon: Icon(Icons.phone_android_rounded,
color: cs.onSurfaceVariant, size: 20),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 14),
),
onSubmitted: (_) => _performSearch(),
),
),
Padding(
padding: const EdgeInsets.all(6.0),
child: Material(
color: cs.primary,
borderRadius: BorderRadius.circular(10),
child: InkWell(
onTap: _performSearch,
borderRadius: BorderRadius.circular(10),
child: const Padding(
padding: EdgeInsets.all(10),
child: Icon(Icons.search_rounded,
color: Colors.white, size: 20),
),
),
),
),
],
),
),
);
}
void _performSearch() {
final phone = searchController.text.trim();
if (phone.isNotEmpty) {
@@ -188,37 +163,98 @@ class CaptainsPage extends StatelessWidget {
}
}
// ================= RESULTS =================
Widget _buildLoadingState(ColorScheme cs) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 40,
height: 40,
child: CircularProgressIndicator(
color: cs.primary,
strokeWidth: 2,
backgroundColor: cs.primary.withValues(alpha: 0.1),
),
),
const SizedBox(height: 16),
Text('جاري التحميل...',
style: TextStyle(
color: cs.onSurfaceVariant, fontSize: 13)),
],
),
);
}
Widget _buildResultsList(BuildContext context, List<dynamic> captains) {
Widget _buildEmptyState(ColorScheme cs) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
shape: BoxShape.circle,
),
child: Icon(Icons.person_off_outlined,
size: 48, color: cs.onSurfaceVariant),
),
const SizedBox(height: 16),
Text('لا يوجد كباتن',
style: TextStyle(
color: cs.onSurfaceVariant, fontSize: 14)),
],
),
);
}
Widget _buildResultsList(
BuildContext context, List<dynamic> captains, ColorScheme cs) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 12),
padding: const EdgeInsets.fromLTRB(20, 8, 20, 8),
child: Row(
children: [
Text(
'Search Results'.tr,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.black87,
Container(
width: 3,
height: 14,
decoration: BoxDecoration(
color: cs.primary,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 8),
Text(
'نتائج البحث',
style: TextStyle(
color: cs.onSurface,
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 10),
Expanded(
child: Container(height: 1, color: cs.outline),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
color: cs.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: cs.primary.withValues(alpha: 0.2)),
),
child: Text(
'${captains.length}',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
color: cs.primary,
fontSize: 12,
fontWeight: FontWeight.w700,
),
),
),
@@ -228,12 +264,12 @@ class CaptainsPage extends StatelessWidget {
Expanded(
child: ListView.separated(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.fromLTRB(20, 0, 20, 80),
padding: const EdgeInsets.fromLTRB(16, 0, 16, 80),
itemCount: captains.length,
separatorBuilder: (_, __) => const SizedBox(height: 12),
separatorBuilder: (_, __) => const SizedBox(height: 10),
itemBuilder: (context, index) {
final captain = captains[index] as Map<String, dynamic>;
return _buildModernCaptainCard(context, captain);
return _buildCaptainCard(context, captain, cs);
},
),
),
@@ -241,10 +277,10 @@ class CaptainsPage extends StatelessWidget {
);
}
// ================= CARD =================
Widget _buildModernCaptainCard(
BuildContext context, Map<String, dynamic> captain) {
Widget _buildCaptainCard(
BuildContext context,
Map<String, dynamic> captain,
ColorScheme cs) {
final String fullName =
'${captain['first_name'] ?? ''} ${captain['last_name'] ?? ''}';
final String phone = captain['phone']?.toString() ?? '';
@@ -252,67 +288,103 @@ class CaptainsPage extends StatelessWidget {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: cs.outline),
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(20),
borderRadius: BorderRadius.circular(16),
onTap: () {
Get.to(() => const CaptainDetailsPage(),
arguments: {'data': captain});
},
child: Padding(
padding: const EdgeInsets.all(16.0),
padding: const EdgeInsets.all(14),
child: Row(
children: [
Container(
width: 56,
height: 56,
width: 52,
height: 52,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Theme.of(context).primaryColor.withOpacity(0.8),
Theme.of(context).primaryColor,
cs.primary.withValues(alpha: 0.8),
cs.primary,
],
),
borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(14),
boxShadow: [
BoxShadow(
color: cs.primary.withValues(alpha: 0.2),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: const Icon(
Icons.person_rounded,
color: Colors.white,
size: 28,
size: 26,
),
),
const SizedBox(width: 16),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
fullName,
style: const TextStyle(
fontSize: 16,
style: TextStyle(
color: cs.onSurface,
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 6),
Text(phone),
Row(
children: [
Icon(Icons.phone_rounded,
size: 13, color: cs.onSurfaceVariant),
const SizedBox(width: 4),
Text(
phone,
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 12,
fontFamily: 'monospace',
),
),
],
),
if (isSuperAdmin && email != null) ...[
const SizedBox(height: 4),
Text(email),
const SizedBox(height: 3),
Row(
children: [
Icon(Icons.email_outlined,
size: 12,
color: cs.onSurfaceVariant),
const SizedBox(width: 4),
Text(
email,
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 11,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
],
],
),
),
Icon(Icons.arrow_forward_ios_rounded,
size: 14, color: cs.onSurfaceVariant),
],
),
),
@@ -320,16 +392,4 @@ class CaptainsPage extends StatelessWidget {
),
);
}
// ================= STATES =================
Widget _buildLoadingState() {
return const Center(child: MyCircularProgressIndicator());
}
Widget _buildEmptyState() {
return const Center(
child: Text("No captains found"),
);
}
}
@@ -1,14 +1,12 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:secure_string_operations/secure_string_operations.dart';
import 'package:siro_admin/constant/box_name.dart';
import '../../../constant/info.dart';
import '../../../constant/char_map.dart';
import '../../../controller/drivers/driver_not_active_controller.dart';
import '../../../main.dart';
import '../../../print.dart';
import 'package:siro_admin/constant/info.dart';
import 'package:siro_admin/constant/char_map.dart';
import 'package:siro_admin/controller/drivers/driver_not_active_controller.dart';
import 'package:siro_admin/main.dart';
import 'package:siro_admin/print.dart';
import 'package:siro_admin/constant/theme.dart';
import 'driver_details_not_active_page.dart';
class DriversPendingPage extends StatelessWidget {
@@ -18,35 +16,256 @@ class DriversPendingPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
controller.getDriversPending();
Log.print(
': ${X.r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs).toString().split(AppInformation.addd)[0]}');
return Scaffold(
appBar: AppBar(title: const Text("Drivers Pending")),
body: GetBuilder<DriverController>(
id: 'drivers',
builder: (c) {
if (c.drivers.isEmpty) {
return Center(
child: const Text('no drivers found yet',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
);
}
return ListView.builder(
itemCount: c.drivers.length,
itemBuilder: (ctx, i) {
final d = c.drivers[i];
return ListTile(
title: Text(d["first_name"] + d['last_name'] ?? ""),
subtitle: Text(d["phone"] ?? ""),
onTap: () {
Get.to(() => DriverDetailsPage(driverId: d["id"].toString()));
},
);
},
);
},
backgroundColor: cs.surface,
body: Column(
children: [
_buildAppBar(context, cs),
Expanded(
child: GetBuilder<DriverController>(
id: 'drivers',
builder: (c) {
if (c.isLoading) {
return _buildLoadingState(cs);
}
if (c.drivers.isEmpty) {
return _buildEmptyState(cs);
}
return RefreshIndicator(
onRefresh: () => controller.getDriversPending(),
color: cs.primary,
child: ListView.separated(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 80),
physics: const BouncingScrollPhysics(),
itemCount: c.drivers.length,
separatorBuilder: (_, __) => const SizedBox(height: 10),
itemBuilder: (ctx, i) {
final d = c.drivers[i];
return _buildDriverCard(d, cs);
},
),
);
},
),
),
],
),
);
}
Widget _buildAppBar(BuildContext context, ColorScheme cs) {
return Container(
padding: const EdgeInsets.fromLTRB(16, 50, 16, 16),
decoration: BoxDecoration(
color: cs.surface,
border: Border(bottom: BorderSide(color: cs.outline)),
),
child: Row(
children: [
GestureDetector(
onTap: () => Get.back(),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: cs.outline),
),
child: Icon(Icons.arrow_back_ios_new_rounded,
color: cs.onSurfaceVariant, size: 16),
),
),
const SizedBox(width: 12),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: const Color(0xFFF59E0B).withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: const Color(0xFFF59E0B).withValues(alpha: 0.25)),
),
child: Icon(Icons.hourglass_top_rounded,
color: const Color(0xFFF59E0B), size: 18),
),
const SizedBox(width: 10),
Text(
'طلبات الكباتن المعلقة',
style: TextStyle(
color: cs.onSurface,
fontSize: 18,
fontWeight: FontWeight.w700,
),
),
],
),
);
}
Widget _buildLoadingState(ColorScheme cs) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 40,
height: 40,
child: CircularProgressIndicator(
color: cs.primary,
strokeWidth: 2,
backgroundColor: cs.primary.withValues(alpha: 0.1),
),
),
const SizedBox(height: 16),
Text('جاري تحميل الطلبات...',
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13)),
],
),
);
}
Widget _buildEmptyState(ColorScheme cs) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
shape: BoxShape.circle,
),
child: Icon(Icons.how_to_reg_rounded,
size: 48, color: cs.onSurfaceVariant),
),
const SizedBox(height: 16),
Text('لا يوجد طلبات تسجيل حالياً',
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 14)),
],
),
);
}
Widget _buildDriverCard(dynamic driver, ColorScheme cs) {
final String fullName =
'${driver['first_name'] ?? ''} ${driver['last_name'] ?? ''}'.trim();
final String phone = driver['phone'] ?? '';
return Container(
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: cs.outline),
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: () {
Get.to(() => DriverDetailsPage(driverId: driver['id'].toString()));
},
child: Padding(
padding: const EdgeInsets.all(14),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
const Color(0xFFF59E0B).withValues(alpha: 0.20),
const Color(0xFFF59E0B).withValues(alpha: 0.08),
],
),
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: const Color(0xFFF59E0B).withValues(alpha: 0.2)),
),
child: Center(
child: Text(
fullName.isNotEmpty ? fullName[0].toUpperCase() : 'D',
style: const TextStyle(
color: Color(0xFFF59E0B),
fontWeight: FontWeight.w800,
fontSize: 18,
),
),
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
fullName,
style: TextStyle(
color: cs.onSurface,
fontWeight: FontWeight.w600,
fontSize: 15,
),
),
const SizedBox(height: 4),
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFFF59E0B)
.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(6),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.hourglass_top_rounded,
size: 11,
color: const Color(0xFFF59E0B)),
const SizedBox(width: 3),
const Text(
'معلق',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
color: Color(0xFFF59E0B),
),
),
],
),
),
const SizedBox(width: 8),
Icon(Icons.phone_rounded,
size: 12, color: cs.onSurfaceVariant),
const SizedBox(width: 4),
Text(
phone,
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 12,
fontFamily: 'monospace',
),
),
],
),
],
),
),
Icon(Icons.arrow_forward_ios_rounded,
size: 14, color: cs.onSurfaceVariant),
],
),
),
),
),
);
}
@@ -1,9 +1,7 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:get/get.dart';
import '../../../constant/colors.dart';
import '../../../constant/style.dart';
import '../../../controller/admin/complaint_controller.dart';
import '../../widgets/my_scafold.dart';
import '../../widgets/elevated_btn.dart';
import '../../widgets/my_textField.dart';
@@ -14,81 +12,101 @@ class ComplaintListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MyScafolld(
title: 'إدارة الشكاوى'.tr,
isleading: true,
body: [
Obx(() {
if (controller.delayedComplaints.isNotEmpty) {
return Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColor.danger.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColor.danger.withOpacity(0.3)),
),
child: Column(
children: [
Row(
children: [
const Icon(Icons.warning_amber_rounded, color: AppColor.danger),
const SizedBox(width: 12),
Expanded(
child: Text(
'هناك ${controller.delayedComplaints.length} شكاوى لم يتم حلها منذ أكثر من أسبوع!',
style: AppStyle.body.copyWith(color: AppColor.danger, fontWeight: FontWeight.bold),
final cs = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: cs.surface,
appBar: AppBar(
backgroundColor: cs.surface,
elevation: 0,
centerTitle: true,
title: Text('إدارة الشكاوى'.tr, style: GoogleFonts.inter(fontWeight: FontWeight.w600, color: cs.onSurface)),
leading: GestureDetector(
onTap: () => Get.back(),
child: Icon(Icons.arrow_back_ios_new_rounded, color: cs.onSurface),
),
),
body: Column(
children: [
Obx(() {
if (controller.delayedComplaints.isNotEmpty) {
return Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: cs.error.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: cs.error.withValues(alpha: 0.3)),
),
child: Column(
children: [
Row(
children: [
Icon(Icons.warning_amber_rounded, color: cs.error),
const SizedBox(width: 12),
Expanded(
child: Text(
'هناك ${controller.delayedComplaints.length} شكاوى لم يتم حلها منذ أكثر من أسبوع!',
style: GoogleFonts.inter(color: cs.error, fontSize: 14, fontWeight: FontWeight.bold),
),
),
),
],
),
const SizedBox(height: 12),
Obx(() => MyElevatedButton(
title: controller.showOnlyDelayed.value ? 'عرض جميع الشكاوى' : 'عرض الشكاوى المتأخرة فقط',
onPressed: () => controller.showOnlyDelayed.toggle(),
kolor: AppColor.danger,
)),
],
),
);
}
return const SizedBox.shrink();
}),
Obx(() {
final list = controller.showOnlyDelayed.value ? controller.delayedComplaints : controller.complaintList;
if (controller.isLoading.value && list.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
return RefreshIndicator(
onRefresh: () => controller.getComplaints(),
child: ListView.builder(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 80),
itemCount: list.length,
itemBuilder: (context, index) {
final complaint = list[index];
return _buildComplaintCard(context, complaint);
},
),
);
}),
],
],
),
const SizedBox(height: 12),
Obx(() => MyElevatedButton(
title: controller.showOnlyDelayed.value ? 'عرض جميع الشكاوى' : 'عرض الشكاوى المتأخرة فقط',
onPressed: () => controller.showOnlyDelayed.toggle(),
kolor: cs.error,
)),
],
),
);
}
return const SizedBox.shrink();
}),
Expanded(
child: Obx(() {
final list = controller.showOnlyDelayed.value ? controller.delayedComplaints : controller.complaintList;
if (controller.isLoading.value && list.isEmpty) {
return Center(child: CircularProgressIndicator(color: cs.primary));
}
return RefreshIndicator(
onRefresh: () => controller.getComplaints(),
child: ListView.builder(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 80),
itemCount: list.length,
itemBuilder: (context, index) {
final complaint = list[index];
return _buildComplaintCard(context, complaint);
},
),
);
}),
),
],
),
);
}
Widget _buildComplaintCard(BuildContext context, dynamic c) {
Color statusColor = _getStatusColor(c['statusComplaint']);
final cs = Theme.of(context).colorScheme;
Color statusColor = _getStatusColor(c['statusComplaint'], cs);
return Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: AppStyle.cardDecoration,
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: cs.outline),
),
child: ExpansionTile(
tilePadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
leading: _buildStatusIndicator(c['statusComplaint'], statusColor),
leading: _buildStatusIndicator(c['statusComplaint'], statusColor, cs),
title: Text(
c['description']?.toString() ?? 'بدون وصف',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: AppStyle.title,
style: GoogleFonts.inter(fontWeight: FontWeight.w600, color: cs.onSurface),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -96,18 +114,18 @@ class ComplaintListPage extends StatelessWidget {
const SizedBox(height: 4),
Text(
'النوع: ${c['complaint_type'] ?? 'عام'} | الرحلة: ${c['ride_id']}',
style: AppStyle.caption,
style: GoogleFonts.inter(color: cs.onSurfaceVariant, fontSize: 12),
),
const SizedBox(height: 4),
Row(
children: [
Icon(Icons.person_rounded, size: 12, color: AppColor.textSecondary),
Icon(Icons.person_rounded, size: 12, color: cs.onSurfaceVariant),
const SizedBox(width: 4),
Text(c['passengerName'] ?? 'غير معروف', style: AppStyle.caption),
Text(c['passengerName'] ?? 'غير معروف', style: GoogleFonts.inter(color: cs.onSurfaceVariant, fontSize: 12)),
const SizedBox(width: 12),
Icon(Icons.drive_eta_rounded, size: 12, color: AppColor.textSecondary),
Icon(Icons.drive_eta_rounded, size: 12, color: cs.onSurfaceVariant),
const SizedBox(width: 4),
Text(c['driverName'] ?? 'غير معروف', style: AppStyle.caption),
Text(c['driverName'] ?? 'غير معروف', style: GoogleFonts.inter(color: cs.onSurfaceVariant, fontSize: 12)),
],
),
],
@@ -118,12 +136,12 @@ class ComplaintListPage extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Divider(color: AppColor.divider),
_buildInfoRow('الوصف', c['description'] ?? 'لا يوجد وصف'),
Divider(color: cs.outline),
_buildInfoRow('الوصف', c['description'] ?? 'لا يوجد وصف', cs),
const SizedBox(height: 12),
_buildInfoRow('الحل الحالي', c['resolution'] ?? 'لم يتم الحل بعد'),
_buildInfoRow('الحل الحالي', c['resolution'] ?? 'لم يتم الحل بعد', cs),
const SizedBox(height: 12),
_buildRideDetails(c),
_buildRideDetails(c, cs),
const SizedBox(height: 24),
Row(
children: [
@@ -131,7 +149,7 @@ class ComplaintListPage extends StatelessWidget {
child: MyElevatedButton(
title: 'تحديث الحالة / حل الشكوى',
onPressed: () => _showResolveDialog(context, c),
kolor: AppColor.accent,
kolor: cs.primary,
),
),
],
@@ -144,14 +162,14 @@ class ComplaintListPage extends StatelessWidget {
);
}
Widget _buildStatusIndicator(String? status, Color color) {
Widget _buildStatusIndicator(String? status, Color color, ColorScheme cs) {
return Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: color.withOpacity(0.12),
color: color.withValues(alpha: 0.12),
shape: BoxShape.circle,
border: Border.all(color: color.withOpacity(0.25)),
border: Border.all(color: color.withValues(alpha: 0.25)),
),
child: Icon(
status == 'Resolved' ? Icons.check_circle_rounded : Icons.pending_rounded,
@@ -161,33 +179,33 @@ class ComplaintListPage extends StatelessWidget {
);
}
Widget _buildInfoRow(String label, String value) {
Widget _buildInfoRow(String label, String value, ColorScheme cs) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: AppStyle.caption.copyWith(color: AppColor.accent)),
Text(label, style: GoogleFonts.inter(color: cs.primary, fontSize: 12)),
const SizedBox(height: 4),
Text(value, style: AppStyle.body),
Text(value, style: GoogleFonts.inter(color: cs.onSurface, fontSize: 14)),
],
);
}
Widget _buildRideDetails(dynamic c) {
Widget _buildRideDetails(dynamic c, ColorScheme cs) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColor.surfaceElevated,
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColor.divider),
border: Border.all(color: cs.outline),
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildSmallStat('السعر', '${c['priceOfRide']} ل.س'),
_buildSmallStat('التقييم', '${c['avgRatingDriverFromPassengers'] ?? 0}★'),
_buildSmallStat('النوع', c['ascarType'] ?? 'N/A'),
_buildSmallStat('السعر', '${c['priceOfRide']} ل.س', cs),
_buildSmallStat('التقييم', '${c['avgRatingDriverFromPassengers'] ?? 0}★', cs),
_buildSmallStat('النوع', c['ascarType'] ?? 'N/A', cs),
],
),
],
@@ -195,26 +213,27 @@ class ComplaintListPage extends StatelessWidget {
);
}
Widget _buildSmallStat(String label, String value) {
Widget _buildSmallStat(String label, String value, ColorScheme cs) {
return Column(
children: [
Text(label, style: AppStyle.caption.copyWith(fontSize: 10)),
Text(label, style: GoogleFonts.inter(color: cs.onSurfaceVariant, fontSize: 10)),
const SizedBox(height: 2),
Text(value, style: AppStyle.number.copyWith(fontSize: 12)),
Text(value, style: GoogleFonts.jetBrainsMono(fontWeight: FontWeight.bold, fontSize: 12, color: cs.onSurface)),
],
);
}
Color _getStatusColor(String? status) {
Color _getStatusColor(String? status, ColorScheme cs) {
switch (status) {
case 'Open': return AppColor.danger;
case 'In Progress': return AppColor.warning;
case 'Resolved': return AppColor.success;
default: return AppColor.textSecondary;
case 'Open': return cs.error;
case 'In Progress': return cs.tertiary;
case 'Resolved': return const Color(0xFF10B981);
default: return cs.onSurfaceVariant;
}
}
void _showResolveDialog(BuildContext context, dynamic c) {
final cs = Theme.of(context).colorScheme;
final TextEditingController resController = TextEditingController(text: c['resolution']);
String selectedStatus = c['statusComplaint'] ?? 'Open';
@@ -222,16 +241,16 @@ class ComplaintListPage extends StatelessWidget {
StatefulBuilder(
builder: (context, setModalState) => Container(
padding: const EdgeInsets.all(24),
decoration: const BoxDecoration(
color: AppColor.surfaceElevated,
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('تحديث حالة الشكوى', style: AppStyle.headTitle),
Text('تحديث حالة الشكوى', style: GoogleFonts.cairo(fontWeight: FontWeight.bold, color: cs.onSurface, fontSize: 18)),
const SizedBox(height: 20),
_buildStatusDropdown(selectedStatus, (val) {
_buildStatusDropdown(selectedStatus, cs, (val) {
setModalState(() => selectedStatus = val!);
}),
const SizedBox(height: 20),
@@ -263,21 +282,21 @@ class ComplaintListPage extends StatelessWidget {
);
}
Widget _buildStatusDropdown(String current, Function(String?) onChanged) {
Widget _buildStatusDropdown(String current, ColorScheme cs, Function(String?) onChanged) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: AppColor.surface,
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColor.divider),
border: Border.all(color: cs.outline),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: current,
isExpanded: true,
dropdownColor: AppColor.surfaceElevated,
dropdownColor: cs.surfaceContainerHighest,
items: ['Open', 'In Progress', 'Resolved']
.map((s) => DropdownMenuItem(value: s, child: Text(s.tr, style: AppStyle.body)))
.map((s) => DropdownMenuItem(value: s, child: Text(s.tr, style: GoogleFonts.inter(color: cs.onSurface, fontSize: 14))))
.toList(),
onChanged: onChanged,
),
@@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_admin/constant/theme.dart';
import 'package:siro_admin/controller/admin/dashboard_v2_controller.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:siro_admin/views/widgets/glass_container.dart';
@@ -1,9 +1,7 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:get/get.dart';
import '../../../constant/colors.dart';
import '../../../constant/style.dart';
import '../../../controller/admin/driver_docs_controller.dart';
import '../../widgets/my_scafold.dart';
import '../../widgets/elevated_btn.dart';
import '../../widgets/snackbar.dart';
import '../../../constant/links.dart';
@@ -15,67 +13,86 @@ class DriverDocsReviewPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MyScafolld(
title: 'مراجعة طلبات التسجيل'.tr,
isleading: true,
body: [
Obx(() => controller.isLoading.value && controller.pendingDrivers.isEmpty
? const Center(child: CircularProgressIndicator())
: controller.pendingDrivers.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.how_to_reg_rounded, size: 64, color: AppColor.textMuted),
const SizedBox(height: 16),
Text('لا يوجد طلبات تسجيل حالياً', style: AppStyle.subtitle),
],
),
)
: RefreshIndicator(
onRefresh: () => controller.getPendingDrivers(),
child: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification scrollInfo) {
if (!controller.isLoading.value &&
!controller.isMoreLoading.value &&
scrollInfo.metrics.pixels >= scrollInfo.metrics.maxScrollExtent - 200) {
controller.loadMore();
}
return false;
},
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: controller.pendingDrivers.length + (controller.hasMore.value ? 1 : 0),
itemBuilder: (context, index) {
if (index == controller.pendingDrivers.length) {
return const Padding(
padding: EdgeInsets.all(16.0),
child: Center(child: CircularProgressIndicator()),
);
}
final driver = controller.pendingDrivers[index];
return _buildDriverCard(context, driver);
},
final cs = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: cs.surface,
appBar: AppBar(
backgroundColor: cs.surface,
elevation: 0,
centerTitle: true,
title: Text('مراجعة طلبات التسجيل'.tr, style: GoogleFonts.inter(fontWeight: FontWeight.w600, color: cs.onSurface)),
leading: GestureDetector(
onTap: () => Get.back(),
child: Icon(Icons.arrow_back_ios_new_rounded, color: cs.onSurface),
),
),
body: Obx(() => controller.isLoading.value && controller.pendingDrivers.isEmpty
? Center(child: CircularProgressIndicator(color: cs.primary))
: controller.pendingDrivers.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(shape: BoxShape.circle, color: cs.onSurfaceVariant.withValues(alpha: 0.1)),
child: Icon(Icons.how_to_reg_rounded, size: 48, color: cs.onSurfaceVariant),
),
const SizedBox(height: 16),
Text('لا يوجد طلبات تسجيل حالياً', style: GoogleFonts.inter(color: cs.onSurfaceVariant, fontSize: 14)),
],
),
)
: RefreshIndicator(
onRefresh: () => controller.getPendingDrivers(),
child: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification scrollInfo) {
if (!controller.isLoading.value &&
!controller.isMoreLoading.value &&
scrollInfo.metrics.pixels >= scrollInfo.metrics.maxScrollExtent - 200) {
controller.loadMore();
}
return false;
},
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: controller.pendingDrivers.length + (controller.hasMore.value ? 1 : 0),
itemBuilder: (context, index) {
if (index == controller.pendingDrivers.length) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Center(child: CircularProgressIndicator(color: cs.primary)),
);
}
final driver = controller.pendingDrivers[index];
return _buildDriverCard(context, driver);
},
),
)),
],
),
)),
);
}
Widget _buildDriverCard(BuildContext context, dynamic driver) {
final cs = Theme.of(context).colorScheme;
return Container(
margin: const EdgeInsets.only(bottom: 12),
decoration: AppStyle.cardDecoration,
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: cs.outline),
),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
leading: CircleAvatar(
backgroundColor: AppColor.accentSoft,
child: Text(driver['first_name']?[0] ?? 'D', style: const TextStyle(color: AppColor.accent)),
backgroundColor: cs.primary.withValues(alpha: 0.1),
child: Text(driver['first_name']?[0] ?? 'D', style: TextStyle(color: cs.primary)),
),
title: Text('${driver['first_name']} ${driver['last_name']}', style: AppStyle.title),
subtitle: Text(driver['phone'] ?? '', style: AppStyle.caption),
trailing: const Icon(Icons.arrow_forward_ios_rounded, size: 16, color: AppColor.textSecondary),
title: Text('${driver['first_name']} ${driver['last_name']}', style: GoogleFonts.inter(fontWeight: FontWeight.w600, color: cs.onSurface)),
subtitle: Text(driver['phone'] ?? '', style: GoogleFonts.inter(color: cs.onSurfaceVariant, fontSize: 12)),
trailing: Icon(Icons.arrow_forward_ios_rounded, size: 16, color: cs.onSurfaceVariant),
onTap: () => _showDriverDetails(context, driver['id'].toString()),
),
);
@@ -88,45 +105,59 @@ class DriverDocsReviewPage extends StatelessWidget {
final driver = details['driver'];
final List docs = details['documents'];
Get.to(() => MyScafolld(
title: 'تفاصيل السائق',
isleading: true,
body: [
SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDriverHeader(driver),
const SizedBox(height: 24),
Text('الوثائق المرفوعة', style: AppStyle.title),
const SizedBox(height: 12),
...docs.map((doc) => _buildDocCard(doc)),
const SizedBox(height: 32),
MyElevatedButton(
title: 'اعتماد وتفعيل الحساب',
icon: Icons.check_circle_rounded,
kolor: AppColor.success,
onPressed: () async {
bool success = await controller.approveDriver(id);
if (success) {
Get.back();
mySnackbarSuccess('تم تفعيل حساب السائق بنجاح');
}
},
),
const SizedBox(height: 100),
],
Get.to(() => Builder(builder: (ctx) {
final cs = Theme.of(ctx).colorScheme;
return Scaffold(
backgroundColor: cs.surface,
appBar: AppBar(
backgroundColor: cs.surface,
elevation: 0,
centerTitle: true,
title: Text('تفاصيل السائق', style: GoogleFonts.inter(fontWeight: FontWeight.w600, color: cs.onSurface)),
leading: GestureDetector(
onTap: () => Get.back(),
child: Icon(Icons.arrow_back_ios_new_rounded, color: cs.onSurface),
),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDriverHeader(driver, cs),
const SizedBox(height: 24),
Text('الوثائق المرفوعة', style: GoogleFonts.inter(fontWeight: FontWeight.w600, color: cs.onSurface)),
const SizedBox(height: 12),
...docs.map((doc) => _buildDocCard(doc, cs)),
const SizedBox(height: 32),
MyElevatedButton(
title: 'اعتماد وتفعيل الحساب',
icon: Icons.check_circle_rounded,
kolor: const Color(0xFF10B981),
onPressed: () async {
bool success = await controller.approveDriver(id);
if (success) {
Get.back();
mySnackbarSuccess('تم تفعيل حساب السائق بنجاح');
}
},
),
),
],
));
const SizedBox(height: 100),
],
),
),
);
}));
}
Widget _buildDriverHeader(dynamic driver) {
Widget _buildDriverHeader(dynamic driver, ColorScheme cs) {
return Container(
padding: const EdgeInsets.all(20),
decoration: AppStyle.elevatedCard,
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: cs.outline),
),
child: Column(
children: [
Row(
@@ -135,28 +166,28 @@ class DriverDocsReviewPage extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${driver['first_name']} ${driver['last_name']}', style: AppStyle.headTitle),
Text(driver['phone'] ?? '', style: AppStyle.subtitle),
Text('${driver['first_name']} ${driver['last_name']}', style: GoogleFonts.cairo(fontWeight: FontWeight.bold, color: cs.onSurface, fontSize: 18)),
Text(driver['phone'] ?? '', style: GoogleFonts.inter(color: cs.onSurfaceVariant)),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: AppColor.warning.withOpacity(0.2),
color: cs.tertiary.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(20),
),
child: Text('Pending', style: AppStyle.caption.copyWith(color: AppColor.warning)),
child: Text('Pending', style: GoogleFonts.inter(color: cs.tertiary, fontSize: 12)),
),
],
),
const Divider(height: 32, color: AppColor.divider),
Divider(height: 32, color: cs.outline),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildSmallInfo('الرقم الوطني', driver['national_number'] ?? 'N/A'),
_buildSmallInfo('الجنس', driver['gender'] ?? 'N/A'),
_buildSmallInfo('تاريخ الميلاد', driver['birthdate'] ?? 'N/A'),
_buildSmallInfo('الرقم الوطني', driver['national_number'] ?? 'N/A', cs),
_buildSmallInfo('الجنس', driver['gender'] ?? 'N/A', cs),
_buildSmallInfo('تاريخ الميلاد', driver['birthdate'] ?? 'N/A', cs),
],
),
],
@@ -164,27 +195,30 @@ class DriverDocsReviewPage extends StatelessWidget {
);
}
Widget _buildSmallInfo(String label, String value) {
Widget _buildSmallInfo(String label, String value, ColorScheme cs) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: AppStyle.caption.copyWith(fontSize: 10)),
Text(label, style: GoogleFonts.inter(color: cs.onSurfaceVariant, fontSize: 10)),
const SizedBox(height: 2),
Text(value, style: AppStyle.body.copyWith(fontSize: 12, fontWeight: FontWeight.bold)),
Text(value, style: GoogleFonts.inter(fontSize: 12, fontWeight: FontWeight.bold, color: cs.onSurface)),
],
);
}
Widget _buildDocCard(dynamic doc) {
Widget _buildDocCard(dynamic doc, ColorScheme cs) {
String imageUrl = doc['link'] ?? '';
// Ensure URL is absolute
if (!imageUrl.startsWith('http')) {
imageUrl = '${AppLink.server}/upload/drivers/$imageUrl';
}
return Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: AppStyle.cardDecoration,
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: cs.outline),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -192,9 +226,9 @@ class DriverDocsReviewPage extends StatelessWidget {
padding: const EdgeInsets.all(12),
child: Row(
children: [
const Icon(Icons.file_present_rounded, color: AppColor.accent, size: 20),
Icon(Icons.file_present_rounded, color: cs.primary, size: 20),
const SizedBox(width: 8),
Text(doc['doc_type'] ?? 'وثيقة', style: AppStyle.title.copyWith(fontSize: 14)),
Text(doc['doc_type'] ?? 'وثيقة', style: GoogleFonts.inter(fontWeight: FontWeight.w600, fontSize: 14, color: cs.onSurface)),
],
),
),
@@ -207,8 +241,8 @@ class DriverDocsReviewPage extends StatelessWidget {
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => Container(
height: 200,
color: AppColor.surfaceElevated,
child: const Center(child: Icon(Icons.broken_image_rounded, size: 48, color: AppColor.textMuted)),
color: cs.surfaceContainerHighest,
child: Center(child: Icon(Icons.broken_image_rounded, size: 48, color: cs.onSurfaceVariant)),
),
),
),
@@ -3,30 +3,22 @@ import 'package:get/get.dart';
import 'package:siro_admin/controller/functions/crud.dart';
import 'package:siro_admin/controller/functions/wallet.dart';
import 'package:siro_admin/views/widgets/snackbar.dart';
import 'package:siro_admin/constant/links.dart';
import '../../../constant/links.dart'; // تأكد من المسار
// --- Controller: المسؤول عن المنطق (البحث، الفحص، الإضافة) ---
class DriverGiftCheckerController extends GetxController {
// للتحكم في حقل النص
final TextEditingController phoneController = TextEditingController();
// لعرض النتائج وحالة التحميل
var statusLog = "".obs;
var isLoading = false.obs;
// قائمة السائقين (سنقوم بتحميلها للبحث عن الـ ID)
List<dynamic> driversCache = [];
// 1. تحميل قائمة السائقين لاستخراج الـ ID منها
Future<void> fetchDriverCache() async {
try {
final response = await CRUD().post(
link: '${AppLink.server}/Admin/driver/getDriverGiftPayment.php',
payload: {'phone': phoneController.text.trim()},
);
// print('response: ${response}');
if (response != 'failure') {
driversCache = (response['message']);
@@ -36,7 +28,6 @@ class DriverGiftCheckerController extends GetxController {
}
}
// --- الدالة الرئيسية التي تنفذ العملية المطلوبة ---
Future<void> processDriverGift() async {
String phoneInput = phoneController.text.trim();
@@ -49,13 +40,11 @@ class DriverGiftCheckerController extends GetxController {
statusLog.value = "جاري البحث عن السائق...";
try {
// الخطوة 1: استخراج الـ ID بناءً على رقم الهاتف
var driver = driversCache.firstWhere(
(d) {
String dbPhone =
d['phone'].toString().replaceAll(RegExp(r'[^0-9]'), '');
String inputPhone = phoneInput.replaceAll(RegExp(r'[^0-9]'), '');
// قارن آخر 9 أرقام لتجاوز مشكلة 09 مقابل 963
if (dbPhone.length >= 9 && inputPhone.length >= 9) {
return dbPhone.substring(dbPhone.length - 9) ==
inputPhone.substring(inputPhone.length - 9);
@@ -77,17 +66,8 @@ class DriverGiftCheckerController extends GetxController {
statusLog.value =
"✅ تم العثور على السائق: $driverName (ID: $driverId)\nجاري فحص رصيد الهدايا...";
// الخطوة 2: فحص السيرفر هل الهدية موجودة؟
// bool hasGift = await _checkIfGiftExistsOnServer(driverId);
// if (hasGift) {
// statusLog.value +=
// "\n⚠️ هذا السائق لديه هدية الافتتاح (30,000) مسبقاً. لم يتم اتخاذ إجراء.";
// } else {
// الخطوة 3: إضافة الهدية
statusLog.value += "\n🎁 الهدية غير موجودة. جاري الإضافة...";
await _addGiftToDriver(driverId, phoneInput, "300");
// }
} catch (e) {
statusLog.value = "حدث خطأ غير متوقع: $e";
} finally {
@@ -95,130 +75,258 @@ class DriverGiftCheckerController extends GetxController {
}
}
// دالة إضافة الهدية باستخدام WalletController الموجود عندك
Future<void> _addGiftToDriver(
String driverId, String phone, String amount) async {
final wallet = Get.put(WalletController());
// استخدام الدالة الموجودة في نظامك
await wallet.addDrivergift300('new driver', driverId, amount, phone);
// statusLog.value += "\n✅ تمت إضافة مبلغ $amount ل.س بنجاح!";
// إضافة تنبيه مرئي
// Get.snackbar("تم بنجاح", "تمت إضافة هدية الافتتاح للسائق",
// backgroundColor: Colors.green, colorText: Colors.white);
}
}
// --- View: واجهة المستخدم ---
class DriverGiftCheckPage extends StatelessWidget {
const DriverGiftCheckPage({super.key});
@override
Widget build(BuildContext context) {
// حقن الكنترولر
final controller = Get.put(DriverGiftCheckerController());
final cs = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: const Color(0xFFF8FAFC),
appBar: AppBar(
title: const Text("فحص ومنح هدية الافتتاح",
style: TextStyle(fontWeight: FontWeight.bold)),
backgroundColor: const Color(0xFF0F172A), // نفس لون الهيدر السابق
foregroundColor: Colors.white,
),
body: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
children: [
// كارد الإدخال
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(color: Colors.grey.withOpacity(0.1), blurRadius: 10)
],
),
backgroundColor: cs.surface,
body: Column(
children: [
_buildAppBar(context, cs),
Expanded(
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.all(16),
child: Column(
children: [
const Icon(Icons.card_giftcard,
size: 50, color: Colors.amber),
const SizedBox(height: 10),
const Text(
"أدخل رقم الهاتف للتحقق",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 20),
// حقل الإدخال
TextField(
controller: controller.phoneController,
keyboardType: TextInputType.phone,
decoration: InputDecoration(
hintText: 'مثال: 0912345678',
prefixIcon: const Icon(Icons.phone),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10)),
filled: true,
fillColor: Colors.grey[50],
),
),
const SizedBox(height: 20),
// زر التنفيذ
Obx(() => SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: controller.isLoading.value
? null
: () => controller.processDriverGift(),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0F172A),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
child: controller.isLoading.value
? const CircularProgressIndicator(
color: Colors.white)
: const Text("تحقق ومنح الهدية (30,000)",
style: TextStyle(fontSize: 16)),
),
)),
_buildInputCard(context, controller, cs),
const SizedBox(height: 24),
_buildLogSection(controller, cs),
],
),
),
),
],
),
);
}
const SizedBox(height: 30),
Widget _buildAppBar(BuildContext context, ColorScheme cs) {
return Container(
padding: const EdgeInsets.fromLTRB(16, 50, 16, 16),
decoration: BoxDecoration(
color: cs.surface,
border: Border(bottom: BorderSide(color: cs.outline)),
),
child: Row(
children: [
GestureDetector(
onTap: () => Get.back(),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: cs.outline),
),
child: Icon(Icons.arrow_back_ios_new_rounded,
color: cs.onSurfaceVariant, size: 16),
),
),
const SizedBox(width: 12),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: const Color(0xFFF59E0B).withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: const Color(0xFFF59E0B).withValues(alpha: 0.25)),
),
child: Icon(Icons.card_giftcard_rounded,
color: const Color(0xFFF59E0B), size: 18),
),
const SizedBox(width: 10),
Text(
'هدايا وعروض الكباتن',
style: TextStyle(
color: cs.onSurface,
fontSize: 18,
fontWeight: FontWeight.w700,
),
),
],
),
);
}
// منطقة عرض النتائج (Log)
Expanded(
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.black87,
borderRadius: BorderRadius.circular(12),
),
child: SingleChildScrollView(
child: Obx(() => Text(
controller.statusLog.value.isEmpty
? "بانتظار العملية..."
: controller.statusLog.value,
style: const TextStyle(
color: Colors.greenAccent,
fontFamily: 'monospace',
height: 1.5),
)),
),
Widget _buildInputCard(
BuildContext context, DriverGiftCheckerController controller, ColorScheme cs) {
return Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: cs.outline),
boxShadow: [
BoxShadow(
color: cs.shadow.withValues(alpha: 0.06),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
const Color(0xFFF59E0B).withValues(alpha: 0.20),
const Color(0xFFF59E0B).withValues(alpha: 0.08),
],
),
shape: BoxShape.circle,
border: Border.all(
color: const Color(0xFFF59E0B).withValues(alpha: 0.25)),
),
child: const Icon(Icons.card_giftcard_rounded,
size: 40, color: Color(0xFFF59E0B)),
),
const SizedBox(height: 16),
Text(
"أدخل رقم الهاتف للتحقق",
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 6),
Text(
"سيتم التحقق ومنح هدية الافتتاح (300)",
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 12,
),
),
const SizedBox(height: 20),
Container(
decoration: BoxDecoration(
color: cs.surface,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: cs.outline),
),
child: TextField(
controller: controller.phoneController,
keyboardType: TextInputType.phone,
style: TextStyle(color: cs.onSurface, fontSize: 14),
cursorColor: cs.primary,
decoration: InputDecoration(
hintText: 'مثال: 0912345678',
hintStyle: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
prefixIcon: Icon(Icons.phone_rounded,
color: cs.onSurfaceVariant, size: 20),
border: InputBorder.none,
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
),
),
],
),
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
height: 48,
child: Obx(() => ElevatedButton(
onPressed: controller.isLoading.value
? null
: () => controller.processDriverGift(),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFF59E0B),
foregroundColor: Colors.white,
disabledBackgroundColor: cs.outline,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14)),
elevation: 0,
),
child: controller.isLoading.value
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: Colors.white, strokeWidth: 2),
)
: const Text("تحقق ومنح الهدية",
style: TextStyle(
fontSize: 14, fontWeight: FontWeight.bold)),
)),
),
],
),
);
}
Widget _buildLogSection(DriverGiftCheckerController controller, ColorScheme cs) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: cs.outline),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: cs.primary.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(8),
),
child: Icon(Icons.terminal_rounded,
color: cs.primary, size: 14),
),
const SizedBox(width: 8),
Text(
'سجل العمليات',
style: TextStyle(
color: cs.onSurface,
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: 12),
Container(
width: double.infinity,
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFF0A0A0B),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: cs.outline),
),
constraints: const BoxConstraints(minHeight: 200),
child: SingleChildScrollView(
child: Obx(() => Text(
controller.statusLog.value.isEmpty
? "بانتظار العملية..."
: controller.statusLog.value,
style: const TextStyle(
color: Color(0xFF4ADE80),
fontFamily: 'monospace',
fontSize: 12,
height: 1.6,
),
)),
),
),
],
),
);
}
@@ -21,30 +21,24 @@ class SiroTrackerScreen extends StatefulWidget {
class _SiroTrackerScreenState extends State<SiroTrackerScreen>
with TickerProviderStateMixin {
// === Map Controller ===
final MapController _mapController = MapController();
List<Marker> _markers = [];
// === State Variables ===
bool isLiveMode = true;
bool isLoading = false;
String lastUpdated = "جاري التحميل...";
// === Counters ===
int liveCount = 0;
int dayCount = 0;
Timer? _timer;
// === Animation Controllers ===
late AnimationController _fadeController;
late AnimationController _scaleController;
// === Admin Info ===
String myPhone = box.read(BoxName.adminPhone).toString();
bool get isSuperAdmin =>
myPhone == '963942542053' || myPhone == '963992952235';
// === URLs ===
final String _baseDir = "${AppLink.server}/ride/location/";
@override
@@ -90,10 +84,11 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
}
void _showSnackBar(String message) {
final cs = Theme.of(context).colorScheme;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: const Color(0xFF2C3E50),
backgroundColor: cs.surfaceContainerHighest,
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.all(16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
@@ -160,6 +155,7 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
}
void _buildMarkers(List<dynamic> drivers) {
final cs = Theme.of(context).colorScheme;
List<Marker> newMarkers = [];
for (var d in drivers) {
@@ -192,7 +188,7 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
cancelled: cancelled,
);
},
child: _buildMarkerWidget(heading),
child: _buildMarkerWidget(heading, cs),
),
),
);
@@ -203,7 +199,7 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
});
}
Widget _buildMarkerWidget(double heading) {
Widget _buildMarkerWidget(double heading, ColorScheme cs) {
return Stack(
alignment: Alignment.center,
children: [
@@ -214,17 +210,17 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
shape: BoxShape.circle,
gradient: LinearGradient(
colors: isLiveMode
? [const Color(0xFF27AE60), const Color(0xFF229954)]
: [const Color(0xFF3498DB), const Color(0xFF2980B9)],
? [const Color(0xFF10B981), const Color(0xFF10B981)]
: [cs.tertiary, cs.tertiary],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(
color: (isLiveMode
? const Color(0xFF27AE60)
: const Color(0xFF3498DB))
.withOpacity(0.5),
? const Color(0xFF10B981)
: cs.tertiary)
.withValues(alpha: 0.5),
blurRadius: 12,
spreadRadius: 2,
)
@@ -235,7 +231,7 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
angle: heading * (math.pi / 180),
child: Icon(
Icons.navigation,
color: Colors.white,
color: cs.onPrimary,
size: 26,
),
),
@@ -252,6 +248,7 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
required String completed,
required String cancelled,
}) {
final cs = Theme.of(context).colorScheme;
showDialog(
context: context,
builder: (_) => Dialog(
@@ -260,14 +257,14 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.white, Colors.grey.shade50],
colors: [cs.surface, cs.surfaceContainerHighest],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
color: cs.shadow.withValues(alpha: 0.15),
blurRadius: 30,
spreadRadius: 5,
)
@@ -284,38 +281,38 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
decoration: BoxDecoration(
gradient: LinearGradient(
colors: isLiveMode
? [const Color(0xFF27AE60), const Color(0xFF229954)]
: [const Color(0xFF3498DB), const Color(0xFF2980B9)],
? [const Color(0xFF10B981), const Color(0xFF10B981)]
: [cs.tertiary, cs.tertiary],
),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.person_outline, color: Colors.white, size: 20),
Icon(Icons.person_outline, color: cs.onPrimary, size: 20),
const SizedBox(width: 8),
const Text(
Text(
"معلومات الكابتن",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
color: cs.onPrimary,
),
),
],
),
),
const SizedBox(height: 20),
_buildInfoCard(Icons.person, "الاسم", name),
_buildInfoCard(Icons.person, "الاسم", name, cs),
const SizedBox(height: 12),
_buildInfoCard(Icons.badge, "المعرف", driverId),
_buildInfoCard(Icons.badge, "المعرف", driverId, cs),
const SizedBox(height: 12),
_buildInfoCard(Icons.speed, "السرعة", "$speed كم/س"),
_buildInfoCard(Icons.speed, "السرعة", "$speed كم/س", cs),
const SizedBox(height: 20),
_buildStatsContainer(completed, cancelled),
_buildStatsContainer(completed, cancelled, cs),
if (isSuperAdmin) ...[
const SizedBox(height: 16),
_buildPhoneButton(phone),
_buildPhoneButton(phone, cs),
],
const SizedBox(height: 20),
SizedBox(
@@ -323,8 +320,8 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2C3E50),
foregroundColor: Colors.white,
backgroundColor: cs.onSurface,
foregroundColor: cs.onPrimary,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
@@ -348,19 +345,20 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
);
}
Widget _buildInfoCard(IconData icon, String label, String value) {
Widget _buildInfoCard(
IconData icon, String label, String value, ColorScheme cs) {
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.white,
color: cs.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.grey.shade200,
color: cs.outline,
width: 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
color: cs.shadow.withValues(alpha: 0.05),
blurRadius: 8,
)
],
@@ -370,10 +368,10 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey.shade100,
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: const Color(0xFF2C3E50), size: 20),
child: Icon(icon, color: cs.onSurface, size: 20),
),
const SizedBox(width: 12),
Column(
@@ -383,17 +381,17 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
label,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
color: cs.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
Text(
value,
style: const TextStyle(
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: Color(0xFF2C3E50),
color: cs.onSurface,
),
),
],
@@ -403,34 +401,36 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
);
}
Widget _buildStatsContainer(String completed, String cancelled) {
Widget _buildStatsContainer(
String completed, String cancelled, ColorScheme cs) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.grey.shade50, Colors.grey.shade100],
colors: [cs.surfaceContainerHighest, cs.surfaceContainerHighest],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade200),
border: Border.all(color: cs.outline),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildStatItem("✓ مكتملة", completed, const Color(0xFF27AE60)),
_buildStatItem("✓ مكتملة", completed, const Color(0xFF10B981), cs),
Container(
width: 1,
height: 40,
color: Colors.grey.shade300,
color: cs.outline,
),
_buildStatItem("✕ ملغاة", cancelled, const Color(0xFFE74C3C)),
_buildStatItem("✕ ملغاة", cancelled, cs.error, cs),
],
),
);
}
Widget _buildStatItem(String label, String value, Color color) {
Widget _buildStatItem(
String label, String value, Color color, ColorScheme cs) {
return Column(
children: [
Text(
@@ -446,7 +446,7 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
label,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
color: cs.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
@@ -454,7 +454,7 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
);
}
Widget _buildPhoneButton(String phone) {
Widget _buildPhoneButton(String phone, ColorScheme cs) {
return InkWell(
onTap: () {
if (phone.isNotEmpty) _makePhoneCall(phone);
@@ -465,14 +465,14 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 14),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [const Color(0xFFFFA500), const Color(0xFFFF8C00)],
colors: [cs.tertiary, cs.tertiary],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: const Color(0xFFFFA500).withOpacity(0.4),
color: cs.tertiary.withValues(alpha: 0.4),
blurRadius: 12,
spreadRadius: 2,
)
@@ -481,13 +481,13 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.call, color: Colors.white, size: 20),
Icon(Icons.call, color: cs.onPrimary, size: 20),
const SizedBox(width: 10),
Expanded(
child: Text(
phone,
style: const TextStyle(
color: Colors.white,
style: TextStyle(
color: cs.onPrimary,
fontWeight: FontWeight.bold,
fontSize: 15,
),
@@ -502,6 +502,7 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Scaffold(
extendBodyBehindAppBar: true,
appBar: AppBar(
@@ -511,20 +512,19 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
title: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFF2C3E50).withOpacity(0.9),
color: cs.onSurface.withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(12),
// backdropFilter: const BackdropFilter(blur: 10),
),
child: const Text(
child: Text(
"نظام تتبع الكابتن",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
color: cs.onPrimary,
),
),
),
foregroundColor: Colors.white,
foregroundColor: cs.onPrimary,
),
body: Stack(
children: [
@@ -542,13 +542,13 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
MarkerLayer(markers: _markers),
],
),
_buildDashboard(),
_buildDashboard(cs),
],
),
);
}
Widget _buildDashboard() {
Widget _buildDashboard(ColorScheme cs) {
return Positioned(
top: 100,
right: 16,
@@ -559,11 +559,11 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
child: Container(
width: 300,
decoration: BoxDecoration(
color: Colors.white,
color: cs.surface,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
color: cs.shadow.withValues(alpha: 0.15),
blurRadius: 30,
spreadRadius: 5,
)
@@ -573,15 +573,11 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
borderRadius: BorderRadius.circular(20),
child: Column(
children: [
// Header
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
const Color(0xFF2C3E50),
const Color(0xFF34495E)
],
colors: [cs.onSurface, cs.onSurfaceVariant],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
@@ -589,12 +585,11 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Icon(Icons.dashboard,
color: Colors.white, size: 22),
const Text(
Icon(Icons.dashboard, color: cs.onPrimary, size: 22),
Text(
"لوحة التحكم",
style: TextStyle(
color: Colors.white,
color: cs.onPrimary,
fontSize: 16,
fontWeight: FontWeight.bold,
),
@@ -607,7 +602,6 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
// Mode Buttons
Row(
children: [
Expanded(
@@ -618,7 +612,8 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
setState(() => isLiveMode = false);
fetchData();
},
const Color(0xFF3498DB),
cs.tertiary,
cs,
),
),
const SizedBox(width: 10),
@@ -630,36 +625,35 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
setState(() => isLiveMode = true);
fetchData();
},
const Color(0xFF27AE60),
const Color(0xFF10B981),
cs,
),
),
],
),
const SizedBox(height: 16),
// Stats
_buildStatRow(
icon: Icons.live_tv,
label: "نشط الآن (مباشر)",
value: liveCount.toString(),
color: const Color(0xFF27AE60),
color: const Color(0xFF10B981),
cs: cs,
),
const SizedBox(height: 12),
_buildStatRow(
icon: Icons.history,
label: "إجمالي اليوم",
value: dayCount.toString(),
color: const Color(0xFF3498DB),
color: cs.tertiary,
cs: cs,
),
const SizedBox(height: 14),
// Last Update
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.grey.shade50,
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade200),
border: Border.all(color: cs.outline),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -670,7 +664,7 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
: "تحديث: $lastUpdated",
style: TextStyle(
fontSize: 11,
color: Colors.grey.shade600,
color: cs.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
@@ -679,23 +673,24 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
? Icons.hourglass_bottom
: Icons.check_circle,
size: 14,
color: isLoading ? Colors.orange : Colors.green,
color: isLoading
? cs.tertiary
: const Color(0xFF10B981),
),
],
),
),
const SizedBox(height: 12),
// Refresh Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: isLoading ? null : fetchData,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2C3E50),
foregroundColor: Colors.white,
disabledBackgroundColor: Colors.grey.shade300,
padding: const EdgeInsets.symmetric(vertical: 12),
backgroundColor: cs.onSurface,
foregroundColor: cs.onPrimary,
disabledBackgroundColor: cs.outline,
padding:
const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
@@ -705,13 +700,14 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (isLoading)
const SizedBox(
SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white,
valueColor:
AlwaysStoppedAnimation<Color>(
cs.onPrimary,
),
),
)
@@ -746,6 +742,7 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
bool active,
VoidCallback onTap,
Color color,
ColorScheme cs,
) {
return InkWell(
onTap: onTap,
@@ -757,21 +754,21 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
decoration: BoxDecoration(
gradient: active
? LinearGradient(
colors: [color, color.withOpacity(0.8)],
colors: [color, color.withValues(alpha: 0.8)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
)
: null,
color: active ? null : Colors.grey.shade100,
color: active ? null : cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: active ? color : Colors.grey.shade300,
color: active ? color : cs.outline,
width: 1.5,
),
boxShadow: active
? [
BoxShadow(
color: color.withOpacity(0.3),
color: color.withValues(alpha: 0.3),
blurRadius: 8,
spreadRadius: 1,
)
@@ -781,7 +778,7 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
child: Text(
title,
style: TextStyle(
color: active ? Colors.white : Colors.grey.shade700,
color: active ? cs.onPrimary : cs.onSurfaceVariant,
fontWeight: active ? FontWeight.bold : FontWeight.w600,
fontSize: 13,
),
@@ -795,13 +792,14 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
required String label,
required String value,
required Color color,
required ColorScheme cs,
}) {
return Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: color, size: 18),
@@ -815,7 +813,7 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
label,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
color: cs.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
@@ -4,14 +4,9 @@ import 'package:get/get.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:siro_admin/constant/links.dart';
// Keep your specific imports
import 'package:siro_admin/controller/functions/crud.dart';
import 'package:siro_admin/views/widgets/snackbar.dart';
/// --------------------------------------------------------------------------
/// 1. DATA MODELS
/// --------------------------------------------------------------------------
class DriverLocation {
final double latitude;
final double longitude;
@@ -38,32 +33,21 @@ class DriverLocation {
}
}
/// --------------------------------------------------------------------------
/// 2. GETX CONTROLLER
/// --------------------------------------------------------------------------
/// تطبيع رقم الهاتف تلقائياً حسب الدولة
/// مثال: 0992952235 ← 963992952235 (سوريا)
/// مثال: 079XXXXXXX ← 96279XXXXXXX (أردن)
/// مثال: 010XXXXXXXX ← 2010XXXXXXXX (مصر)
String normalizePhone(String input) {
final clean = input.replaceAll(RegExp(r'\D+'), '');
// Syria: 099XXXXXXX or 9639XXXXXXX
if (clean.length == 10 && clean.startsWith('09')) {
return '963${clean.substring(1)}';
}
if (clean.length == 12 && clean.startsWith('963')) return clean;
if (clean.length == 9 && clean.startsWith('9')) return '963$clean';
// Jordan: 079XXXXXXX or 9627XXXXXXX
if (clean.length == 10 && clean.startsWith('07')) {
return '962${clean.substring(1)}';
}
if (clean.length == 12 && clean.startsWith('962')) return clean;
if (clean.length == 9 && clean.startsWith('7')) return '962$clean';
// Egypt: 010XXXXXXXX or 2010XXXXXXXX
if (clean.length == 11 && clean.startsWith('01')) {
return '20${clean.substring(1)}';
}
@@ -73,32 +57,26 @@ String normalizePhone(String input) {
}
class RideMonitorController extends GetxController {
// CONFIGURATION
final String apiUrl = "${AppLink.server}/Admin/rides/monitorRide.php";
// INPUT CONTROLLERS
final TextEditingController phoneInputController = TextEditingController();
// OBSERVABLES
var isTracking = false.obs;
var isLoading = false.obs;
var hasError = false.obs;
var errorMessage = ''.obs;
// Driver & Ride Data
var driverLocation = Rxn<DriverLocation>();
var driverName = "Unknown Driver".obs;
var rideStatus = "Waiting...".obs;
// Route Data
var startPoint = Rxn<LatLng>();
var endPoint = Rxn<LatLng>();
var routePolyline = <LatLng>[].obs; // List of points for the line
var routePolyline = <LatLng>[].obs;
// Map Variables
final MapController mapController = MapController();
Timer? _timer;
bool _isFirstLoad = true; // To trigger auto-fit bounds only on first success
bool _isFirstLoad = true;
@override
void onClose() {
@@ -107,15 +85,12 @@ class RideMonitorController extends GetxController {
super.onClose();
}
// --- ACTIONS ---
void startSearch() {
if (phoneInputController.text.trim().isEmpty) {
mySnackbarWarning("يرجى إدخال رقم الهاتف أولاً");
return;
}
// Reset state
hasError.value = false;
errorMessage.value = '';
driverLocation.value = null;
@@ -126,14 +101,11 @@ class RideMonitorController extends GetxController {
rideStatus.value = "جاري التحميل...";
_isFirstLoad = true;
// Switch UI
isTracking.value = true;
isLoading.value = true;
// Start fetching
fetchRideData();
// Start Polling
_timer?.cancel();
_timer = Timer.periodic(const Duration(seconds: 10), (timer) {
fetchRideData();
@@ -144,7 +116,6 @@ class RideMonitorController extends GetxController {
_timer?.cancel();
isTracking.value = false;
isLoading.value = false;
// phoneInputController.clear(); // اختياري: يمكنك إبقائه لتسهيل البحث مرة أخرى
}
Future<void> fetchRideData() async {
@@ -152,7 +123,6 @@ class RideMonitorController extends GetxController {
if (phone.isEmpty) return;
try {
// تطبيع رقم الهاتف تلقائياً حسب الدولة
String normalizedPhone = normalizePhone(phone);
final response = await CRUD().post(
link: apiUrl,
@@ -168,17 +138,14 @@ class RideMonitorController extends GetxController {
final data =
jsonResponse['message'] ?? jsonResponse['data'] ?? jsonResponse;
// 1. Parse Driver Info
if (data['driver_details'] != null) {
driverName.value =
data['driver_details']['fullname'] ?? "سائق غير معروف";
}
// 2. Parse Ride Info & Route
if (data['ride_details'] != null) {
rideStatus.value = data['ride_details']['status'] ?? "غير معروف";
// Parse Start/End Locations (Format: "lat,lng")
String? startStr = data['ride_details']['start_location'];
String? endStr = data['ride_details']['end_location'];
@@ -188,20 +155,17 @@ class RideMonitorController extends GetxController {
if (s != null && e != null) {
startPoint.value = s;
endPoint.value = e;
routePolyline.value = [s, e]; // Straight line for now
routePolyline.value = [s, e];
}
}
// 3. Parse Live Location
final locData = data['driver_location'];
if (locData is Map<String, dynamic>) {
final newLocation = DriverLocation.fromJson(locData);
driverLocation.value = newLocation;
// 4. Update Camera Bounds
_updateMapBounds();
} else {
// Even if no live driver, we might want to show the route
if (startPoint.value != null && endPoint.value != null) {
_updateMapBounds();
}
@@ -227,7 +191,6 @@ class RideMonitorController extends GetxController {
}
}
// Helper to parse "lat,lng" string
LatLng? _parseLatLngString(String? str) {
if (str == null || !str.contains(',')) return null;
try {
@@ -240,7 +203,6 @@ class RideMonitorController extends GetxController {
}
}
// Logic to fit start, end, and driver on screen
void _updateMapBounds() {
if (!_isFirstLoad) return;
@@ -264,48 +226,36 @@ class RideMonitorController extends GetxController {
);
_isFirstLoad = false;
} catch (e) {
// Map Controller not ready yet
}
}
}
}
/// --------------------------------------------------------------------------
/// 3. UI SCREEN (Modern Light Theme)
/// --------------------------------------------------------------------------
class RideMonitorScreen extends StatelessWidget {
const RideMonitorScreen({super.key});
// 🎨 الألوان العصرية (Modern Palette)
final Color backgroundColor = const Color(0xFFF4F7FE);
final Color primaryColor = const Color(0xFF4318FF);
final Color textPrimary = const Color(0xFF2B3674);
final Color textSecondary = const Color(0xFFA3AED0);
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final RideMonitorController controller = Get.put(RideMonitorController());
return Scaffold(
backgroundColor: backgroundColor,
// الإبقاء على AppBar فقط في شاشة البحث
backgroundColor: cs.surface,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight),
child: Obx(() {
if (controller.isTracking.value) {
return const SizedBox
.shrink(); // إخفاء الـ AppBar في وضع التتبع للخريطة الكاملة
return const SizedBox.shrink();
}
return AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
centerTitle: true,
iconTheme: IconThemeData(color: textPrimary),
iconTheme: IconThemeData(color: cs.onSurface),
title: Text(
"مراقبة الرحلات",
style: TextStyle(
color: textPrimary,
color: cs.onSurface,
fontWeight: FontWeight.bold,
fontSize: 18,
),
@@ -315,18 +265,15 @@ class RideMonitorScreen extends StatelessWidget {
),
body: Obx(() {
if (!controller.isTracking.value) {
return _buildSearchForm(context, controller);
return _buildSearchForm(context, controller, cs);
}
return _buildMapTrackingView(context, controller);
return _buildMapTrackingView(context, controller, cs);
}),
);
}
// ---------------------------------------------------------------------------
// واجهة البحث (Search View)
// ---------------------------------------------------------------------------
Widget _buildSearchForm(
BuildContext context, RideMonitorController controller) {
BuildContext context, RideMonitorController controller, ColorScheme cs) {
return Center(
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
@@ -335,11 +282,11 @@ class RideMonitorScreen extends StatelessWidget {
child: Container(
padding: const EdgeInsets.all(32.0),
decoration: BoxDecoration(
color: Colors.white,
color: cs.surface,
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: primaryColor.withOpacity(0.08),
color: cs.primary.withValues(alpha: 0.08),
blurRadius: 24,
offset: const Offset(0, 10),
)
@@ -351,17 +298,16 @@ class RideMonitorScreen extends StatelessWidget {
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: primaryColor.withOpacity(0.1),
color: cs.primary.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child:
Icon(Icons.radar_rounded, size: 60, color: primaryColor),
child: Icon(Icons.radar_rounded, size: 60, color: cs.primary),
),
const SizedBox(height: 24),
Text(
"تتبع رحلة نشطة",
style: TextStyle(
color: textPrimary,
color: cs.onSurface,
fontSize: 22,
fontWeight: FontWeight.bold,
),
@@ -370,7 +316,7 @@ class RideMonitorScreen extends StatelessWidget {
Text(
"أدخل رقم هاتف السائق أو الراكب للبدء",
style: TextStyle(
color: textSecondary,
color: cs.onSurfaceVariant,
fontSize: 14,
),
textAlign: TextAlign.center,
@@ -378,27 +324,27 @@ class RideMonitorScreen extends StatelessWidget {
const SizedBox(height: 32),
Container(
decoration: BoxDecoration(
color: backgroundColor,
color: cs.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.white, width: 2),
border: Border.all(color: cs.outline, width: 2),
),
child: TextField(
controller: controller.phoneInputController,
keyboardType: TextInputType.phone,
textDirection: TextDirection.ltr,
style: TextStyle(
color: textPrimary,
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w600,
),
decoration: InputDecoration(
hintText: "مثال: 0992952235...",
hintStyle: TextStyle(color: textSecondary),
hintStyle: TextStyle(color: cs.onSurfaceVariant),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(
vertical: 18, horizontal: 20),
prefixIcon:
Icon(Icons.phone_rounded, color: primaryColor),
Icon(Icons.phone_rounded, color: cs.primary),
),
),
),
@@ -409,16 +355,16 @@ class RideMonitorScreen extends StatelessWidget {
child: ElevatedButton(
onPressed: controller.startSearch,
style: ElevatedButton.styleFrom(
backgroundColor: primaryColor,
backgroundColor: cs.primary,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: const Text(
child: Text(
"بدء المراقبة",
style: TextStyle(
color: Colors.white,
color: cs.onPrimary,
fontSize: 16,
fontWeight: FontWeight.bold,
),
@@ -433,11 +379,8 @@ class RideMonitorScreen extends StatelessWidget {
);
}
// ---------------------------------------------------------------------------
// واجهة الخريطة (Map View)
// ---------------------------------------------------------------------------
Widget _buildMapTrackingView(
BuildContext context, RideMonitorController controller) {
BuildContext context, RideMonitorController controller, ColorScheme cs) {
return Stack(
children: [
FlutterMap(
@@ -451,45 +394,36 @@ class RideMonitorScreen extends StatelessWidget {
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'com.siromove.admin',
),
// 1. ROUTE LINE (Polyline)
if (controller.routePolyline.isNotEmpty)
PolylineLayer(
polylines: [
Polyline(
points: controller.routePolyline,
strokeWidth: 6.0,
color: primaryColor.withOpacity(0.9),
color: cs.primary.withValues(alpha: 0.9),
borderStrokeWidth: 2.0,
borderColor: primaryColor.withOpacity(0.3),
borderColor: cs.primary.withValues(alpha: 0.3),
strokeCap: StrokeCap.round,
strokeJoin: StrokeJoin.round,
),
],
),
// 2. START & END MARKERS
MarkerLayer(
markers: [
// Start Point (Green Dot)
if (controller.startPoint.value != null)
Marker(
point: controller.startPoint.value!,
width: 30,
height: 30,
child: _buildPointMarker(const Color(0xFF10B981)),
child: _buildPointMarker(const Color(0xFF10B981), cs),
),
// End Point (Red Dot)
if (controller.endPoint.value != null)
Marker(
point: controller.endPoint.value!,
width: 30,
height: 30,
child: _buildPointMarker(const Color(0xFFEF4444)),
child: _buildPointMarker(const Color(0xFFEF4444), cs),
),
// Driver Car Marker
if (controller.driverLocation.value != null)
Marker(
point: LatLng(
@@ -507,11 +441,11 @@ class RideMonitorScreen extends StatelessWidget {
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white,
color: cs.surface,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
color: cs.onSurface.withValues(alpha: 0.2),
blurRadius: 10,
spreadRadius: 2,
)
@@ -519,7 +453,7 @@ class RideMonitorScreen extends StatelessWidget {
),
child: Icon(
Icons.directions_car_rounded,
color: primaryColor,
color: cs.primary,
size: 28,
),
),
@@ -528,13 +462,13 @@ class RideMonitorScreen extends StatelessWidget {
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: textPrimary,
color: cs.onSurface,
borderRadius: BorderRadius.circular(6),
),
child: Text(
"${controller.driverLocation.value!.speed.toInt()} كم",
style: const TextStyle(
color: Colors.white,
style: TextStyle(
color: cs.onPrimary,
fontSize: 10,
fontWeight: FontWeight.bold,
),
@@ -549,48 +483,44 @@ class RideMonitorScreen extends StatelessWidget {
),
],
),
// زر التراجع (إيقاف التتبع) أعلى الشاشة
Positioned(
top: MediaQuery.of(context).padding.top + 10,
right: 20, // أو left حسب لغة التطبيق
right: 20,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
color: cs.surface,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
color: cs.onSurface.withValues(alpha: 0.1),
blurRadius: 10,
offset: const Offset(0, 4),
)
],
),
child: IconButton(
icon: Icon(Icons.close_rounded, color: textPrimary, size: 24),
icon: Icon(Icons.close_rounded, color: cs.onSurface, size: 24),
onPressed: controller.stopTracking,
tooltip: "إيقاف المراقبة",
),
),
),
// LOADING OVERLAY (Smooth Frosted Glass like)
if (controller.isLoading.value &&
controller.driverLocation.value == null &&
controller.startPoint.value == null)
Container(
color: Colors.white.withOpacity(0.8),
color: cs.surface.withValues(alpha: 0.8),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(
color: primaryColor, strokeWidth: 3),
color: cs.primary, strokeWidth: 3),
const SizedBox(height: 16),
Text(
"جاري تحديد الموقع...",
style: TextStyle(
color: textPrimary,
color: cs.onSurface,
fontWeight: FontWeight.bold,
fontSize: 16,
),
@@ -599,19 +529,17 @@ class RideMonitorScreen extends StatelessWidget {
),
),
),
// ERROR OVERLAY
if (controller.hasError.value)
Center(
child: Container(
margin: const EdgeInsets.all(24),
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.white,
color: cs.surface,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
color: cs.onSurface.withValues(alpha: 0.1),
blurRadius: 20,
offset: const Offset(0, 10),
)
@@ -623,17 +551,17 @@ class RideMonitorScreen extends StatelessWidget {
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
color: cs.error.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: const Icon(Icons.error_outline_rounded,
color: Colors.red, size: 40),
child: Icon(Icons.error_outline_rounded,
color: cs.error, size: 40),
),
const SizedBox(height: 16),
Text(
"حدث خطأ",
style: TextStyle(
color: textPrimary,
color: cs.onSurface,
fontSize: 18,
fontWeight: FontWeight.bold,
),
@@ -642,7 +570,7 @@ class RideMonitorScreen extends StatelessWidget {
Text(
controller.errorMessage.value,
textAlign: TextAlign.center,
style: TextStyle(color: textSecondary, height: 1.5),
style: TextStyle(color: cs.onSurfaceVariant, height: 1.5),
),
const SizedBox(height: 24),
SizedBox(
@@ -650,8 +578,8 @@ class RideMonitorScreen extends StatelessWidget {
child: ElevatedButton(
onPressed: controller.stopTracking,
style: ElevatedButton.styleFrom(
backgroundColor: backgroundColor,
foregroundColor: textPrimary,
backgroundColor: cs.surface,
foregroundColor: cs.onSurface,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
@@ -666,8 +594,6 @@ class RideMonitorScreen extends StatelessWidget {
),
),
),
// INFO CARD (Bottom Floating Card)
if (!controller.hasError.value && !controller.isLoading.value)
Positioned(
bottom: 30,
@@ -675,11 +601,11 @@ class RideMonitorScreen extends StatelessWidget {
right: 20,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
color: cs.surface,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
color: cs.onSurface.withValues(alpha: 0.08),
blurRadius: 24,
offset: const Offset(0, 10),
)
@@ -696,11 +622,11 @@ class RideMonitorScreen extends StatelessWidget {
width: 50,
height: 50,
decoration: BoxDecoration(
color: primaryColor.withOpacity(0.1),
color: cs.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(15),
),
child: Icon(Icons.person_rounded,
color: primaryColor, size: 28),
color: cs.primary, size: 28),
),
const SizedBox(width: 16),
Expanded(
@@ -710,7 +636,7 @@ class RideMonitorScreen extends StatelessWidget {
Text(
controller.driverName.value,
style: TextStyle(
color: textPrimary,
color: cs.onSurface,
fontSize: 18,
fontWeight: FontWeight.bold,
),
@@ -735,7 +661,7 @@ class RideMonitorScreen extends StatelessWidget {
Text(
controller.rideStatus.value,
style: TextStyle(
color: textSecondary,
color: cs.onSurfaceVariant,
fontSize: 13,
fontWeight: FontWeight.w600,
),
@@ -759,17 +685,19 @@ class RideMonitorScreen extends StatelessWidget {
Icons.speed_rounded,
"${controller.driverLocation.value!.speed.toStringAsFixed(1)} كم/س",
const Color(0xFF3B82F6),
cs,
),
Container(
width: 1,
height: 30,
color: Colors.grey.withOpacity(0.2)),
color: cs.outline.withValues(alpha: 0.2)),
_buildModernInfoBadge(
Icons.access_time_rounded,
controller.driverLocation.value!.updatedAt
.split(' ')
.last,
const Color(0xFF8B5CF6),
cs,
),
],
)
@@ -781,13 +709,13 @@ class RideMonitorScreen extends StatelessWidget {
width: 16,
height: 16,
child: CircularProgressIndicator(
color: primaryColor, strokeWidth: 2),
color: cs.primary, strokeWidth: 2),
),
const SizedBox(width: 10),
Text(
"جاري الاتصال بالسائق...",
style: TextStyle(
color: primaryColor,
color: cs.primary,
fontWeight: FontWeight.w600,
fontSize: 13,
),
@@ -803,12 +731,10 @@ class RideMonitorScreen extends StatelessWidget {
);
}
// --- Helper Widgets ---
Widget _buildPointMarker(Color color) {
Widget _buildPointMarker(Color color, ColorScheme cs) {
return Container(
decoration: BoxDecoration(
color: color.withOpacity(0.3),
color: color.withValues(alpha: 0.3),
shape: BoxShape.circle,
),
child: Center(
@@ -818,10 +744,10 @@ class RideMonitorScreen extends StatelessWidget {
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
border: Border.all(color: cs.surface, width: 2),
boxShadow: [
BoxShadow(
color: color.withOpacity(0.5),
color: color.withValues(alpha: 0.5),
blurRadius: 6,
spreadRadius: 1,
)
@@ -832,13 +758,14 @@ class RideMonitorScreen extends StatelessWidget {
);
}
Widget _buildModernInfoBadge(IconData icon, String text, Color iconColor) {
Widget _buildModernInfoBadge(
IconData icon, String text, Color iconColor, ColorScheme cs) {
return Row(
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: iconColor.withOpacity(0.1),
color: iconColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, size: 16, color: iconColor),
@@ -847,11 +774,11 @@ class RideMonitorScreen extends StatelessWidget {
Text(
text,
style: TextStyle(
color: textPrimary,
color: cs.onSurface,
fontSize: 14,
fontWeight: FontWeight.bold,
),
textDirection: TextDirection.ltr, // للحفاظ على اتجاه الأرقام
textDirection: TextDirection.ltr,
),
],
);
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_admin/constant/links.dart';
import 'package:siro_admin/controller/employee_controller/employee_controller.dart';
import 'package:siro_admin/controller/functions/upload_image copy.dart'; // تأكد من مسار الملف الصحيح
import 'package:siro_admin/controller/functions/upload_image copy.dart';
import 'package:url_launcher/url_launcher.dart';
class EmployeePage extends StatelessWidget {
@@ -10,27 +10,21 @@ class EmployeePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// حقن الكنترولر
Get.put(EmployeeController());
// ألوان الثيم
const Color bgColor = Color(0xFF0A0E27);
const Color cardColor = Color(0xFF1A1F3A);
const Color primaryAccent = Color(0xFF6366F1);
final cs = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: bgColor,
backgroundColor: cs.surface,
body: GetBuilder<EmployeeController>(
builder: (controller) {
return CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: [
// 1. App Bar
SliverAppBar(
expandedHeight: 100,
floating: true,
pinned: true,
backgroundColor: bgColor,
backgroundColor: cs.surface,
elevation: 0,
flexibleSpace: FlexibleSpaceBar(
titlePadding: const EdgeInsets.only(bottom: 16),
@@ -40,19 +34,19 @@ class EmployeePage extends StatelessWidget {
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: primaryAccent.withOpacity(0.2),
color: cs.primary.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(Icons.badge_rounded,
color: Colors.white, size: 18),
child: Icon(Icons.badge_rounded,
color: cs.onSurface, size: 18),
),
const SizedBox(width: 10),
const Text(
Text(
'الموظفون',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Colors.white,
color: cs.onSurface,
fontFamily: 'Segoe UI',
),
),
@@ -65,8 +59,8 @@ class EmployeePage extends StatelessWidget {
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
primaryAccent.withOpacity(0.15),
bgColor,
cs.primary.withValues(alpha: 0.15),
cs.surface,
],
),
),
@@ -74,18 +68,17 @@ class EmployeePage extends StatelessWidget {
),
),
// 2. قائمة الموظفين
if (controller.employee.isEmpty)
const SliverFillRemaining(
SliverFillRemaining(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.people_outline,
size: 60, color: Colors.white24),
SizedBox(height: 16),
size: 60, color: cs.onSurface.withValues(alpha: 0.24)),
const SizedBox(height: 16),
Text("لا يوجد موظفين حالياً",
style: TextStyle(color: Colors.white54)),
style: TextStyle(color: cs.onSurfaceVariant)),
],
),
),
@@ -100,8 +93,7 @@ class EmployeePage extends StatelessWidget {
return _EmployeeCard(
employee: employee,
index: index,
cardColor: cardColor,
primaryAccent: primaryAccent,
cs: cs,
);
},
childCount: controller.employee.length,
@@ -118,25 +110,22 @@ class EmployeePage extends StatelessWidget {
controller.id = controller.generateRandomId(8);
Get.to(() => _EmployeeFormScreen(controller: controller));
},
backgroundColor: primaryAccent,
child: const Icon(Icons.person_add_rounded, color: Colors.white),
backgroundColor: cs.primary,
child: Icon(Icons.person_add_rounded, color: cs.onPrimary),
),
);
}
}
// === بطاقة الموظف (تصميم جديد ومحسن) ===
class _EmployeeCard extends StatelessWidget {
final Map<String, dynamic> employee;
final int index;
final Color cardColor;
final Color primaryAccent;
final ColorScheme cs;
const _EmployeeCard({
required this.employee,
required this.index,
required this.cardColor,
required this.primaryAccent,
required this.cs,
});
@override
@@ -147,12 +136,12 @@ class _EmployeeCard extends StatelessWidget {
return Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: cardColor,
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.white.withOpacity(0.05)),
border: Border.all(color: cs.outline.withValues(alpha: 0.1)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
color: Colors.black.withValues(alpha: 0.2),
blurRadius: 10,
offset: const Offset(0, 4),
),
@@ -168,28 +157,27 @@ class _EmployeeCard extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// الصف العلوي: الحالة + أيقونة
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.05),
color: cs.outline.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: Icon(Icons.person,
color: Colors.white.withOpacity(0.7), size: 20),
color: cs.onSurface.withValues(alpha: 0.7), size: 20),
),
Flexible(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
color: statusColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border:
Border.all(color: statusColor.withOpacity(0.3)),
Border.all(color: statusColor.withValues(alpha: 0.3)),
),
child: Text(
employee['status'] ?? 'Unknown',
@@ -208,23 +196,21 @@ class _EmployeeCard extends StatelessWidget {
const SizedBox(height: 12),
// الاسم في سطر كامل ومميز
Text(
employee['name'] ?? 'Unknown',
style: const TextStyle(
color: Colors.white,
style: TextStyle(
color: cs.onSurface,
fontSize: 18,
fontWeight: FontWeight.bold,
height: 1.3,
),
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 12),
child: Divider(height: 1, color: Colors.white10),
Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Divider(height: 1, color: cs.outline.withValues(alpha: 0.1)),
),
// تفاصيل التعليم والهاتف والموقع (مع دعم تعدد الأسطر)
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
@@ -233,23 +219,22 @@ class _EmployeeCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow(Icons.phone_iphone_rounded,
employee['phone'] ?? '', Colors.white54),
const SizedBox(height: 12), // مسافة أكبر بين العناصر
employee['phone'] ?? '', cs.onSurfaceVariant, cs),
const SizedBox(height: 12),
_buildInfoRow(
Icons.school_outlined,
employee['education'] ?? 'غير محدد',
primaryAccent),
const SizedBox(height: 12), // مسافة أكبر
cs.primary, cs),
const SizedBox(height: 12),
_buildInfoRow(Icons.location_on_outlined,
employee['site'] ?? 'غير محدد', Colors.blueGrey),
employee['site'] ?? 'غير محدد', Colors.blueGrey, cs),
],
),
),
// زر الاتصال الجانبي
const SizedBox(width: 16),
Material(
color: Colors.green.withOpacity(0.1),
color: const Color(0xFF10B981).withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
child: InkWell(
onTap: () =>
@@ -258,7 +243,7 @@ class _EmployeeCard extends StatelessWidget {
child: Container(
padding: const EdgeInsets.all(14),
child: const Icon(Icons.call,
color: Colors.green, size: 24),
color: Color(0xFF10B981), size: 24),
),
),
),
@@ -272,25 +257,23 @@ class _EmployeeCard extends StatelessWidget {
);
}
Widget _buildInfoRow(IconData icon, String text, Color iconColor) {
Widget _buildInfoRow(IconData icon, String text, Color iconColor, ColorScheme cs) {
return Row(
crossAxisAlignment:
CrossAxisAlignment.start, // محاذاة الأيقونة مع بداية النص
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(top: 2), // ضبط بسيط لموقع الأيقونة
child: Icon(icon, size: 16, color: iconColor.withOpacity(0.8)),
padding: const EdgeInsets.only(top: 2),
child: Icon(icon, size: 16, color: iconColor.withValues(alpha: 0.8)),
),
const SizedBox(width: 10),
Expanded(
child: Text(
text,
style: TextStyle(
color: Colors.white.withOpacity(0.7),
color: cs.onSurface.withValues(alpha: 0.7),
fontSize: 13,
height: 1.5, // تباعد الأسطر لسهولة القراءة
height: 1.5,
),
// تم إزالة maxLines و overflow للسماح بالنص بالنزول لأسطر متعددة
),
),
],
@@ -305,25 +288,23 @@ class _EmployeeCard extends StatelessWidget {
}
}
// === شاشة إضافة موظف ===
class _EmployeeFormScreen extends StatelessWidget {
final EmployeeController controller;
const _EmployeeFormScreen({required this.controller});
@override
Widget build(BuildContext context) {
const Color bgColor = Color(0xFF0A0E27);
const Color inputColor = Color(0xFF1A1F3A);
final cs = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: bgColor,
backgroundColor: cs.surface,
appBar: AppBar(
title: const Text("إضافة موظف جديد",
style: TextStyle(color: Colors.white)),
backgroundColor: bgColor,
title: Text("إضافة موظف جديد",
style: TextStyle(color: cs.onSurface)),
backgroundColor: cs.surface,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
icon: Icon(Icons.arrow_back, color: cs.onSurface),
onPressed: () => Get.back(),
),
),
@@ -339,6 +320,7 @@ class _EmployeeFormScreen extends StatelessWidget {
child: _UploadButton(
title: "الهوية (أمام)",
icon: Icons.credit_card,
cs: cs,
onPressed: () async {
await ImageController().choosImage(AppLink.uploadEgypt,
'idFrontEmployee', controller.id);
@@ -350,6 +332,7 @@ class _EmployeeFormScreen extends StatelessWidget {
child: _UploadButton(
title: "الهوية (خلف)",
icon: Icons.credit_card_outlined,
cs: cs,
onPressed: () async {
await ImageController().choosImage(AppLink.uploadEgypt,
'idbackEmployee', controller.id);
@@ -360,20 +343,20 @@ class _EmployeeFormScreen extends StatelessWidget {
),
const SizedBox(height: 24),
_buildModernTextField(
controller.name, "الاسم الكامل", Icons.person, inputColor),
controller.name, "الاسم الكامل", Icons.person, cs),
const SizedBox(height: 16),
_buildModernTextField(
controller.phone, "رقم الهاتف", Icons.phone, inputColor,
controller.phone, "رقم الهاتف", Icons.phone, cs,
type: TextInputType.phone),
const SizedBox(height: 16),
_buildModernTextField(controller.education, "التعليم / الملاحظات",
Icons.school, inputColor),
Icons.school, cs),
const SizedBox(height: 16),
_buildModernTextField(controller.site, "الموقع / العنوان",
Icons.location_on, inputColor),
Icons.location_on, cs),
const SizedBox(height: 16),
_buildModernTextField(controller.status, "الحالة (مثال: ممتاز)",
Icons.star, inputColor),
Icons.star, cs),
const SizedBox(height: 32),
SizedBox(
width: double.infinity,
@@ -385,14 +368,14 @@ class _EmployeeFormScreen extends StatelessWidget {
}
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF6366F1),
backgroundColor: cs.primary,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
child: const Text("حفظ البيانات",
child: Text("حفظ البيانات",
style: TextStyle(
color: Colors.white,
color: cs.onPrimary,
fontSize: 16,
fontWeight: FontWeight.bold)),
),
@@ -405,23 +388,23 @@ class _EmployeeFormScreen extends StatelessWidget {
}
Widget _buildModernTextField(TextEditingController controller, String hint,
IconData icon, Color fillColor,
IconData icon, ColorScheme cs,
{TextInputType type = TextInputType.text}) {
return Container(
decoration: BoxDecoration(
color: fillColor,
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white.withOpacity(0.1)),
border: Border.all(color: cs.outline.withValues(alpha: 0.1)),
),
child: TextFormField(
controller: controller,
keyboardType: type,
style: const TextStyle(color: Colors.white),
maxLines: null, // السماح بتعدد الأسطر عند الإدخال أيضاً
style: TextStyle(color: cs.onSurface),
maxLines: null,
decoration: InputDecoration(
labelText: hint,
labelStyle: TextStyle(color: Colors.white.withOpacity(0.5)),
prefixIcon: Icon(icon, color: Colors.white38, size: 20),
labelStyle: TextStyle(color: cs.onSurface.withValues(alpha: 0.5)),
prefixIcon: Icon(icon, color: cs.onSurfaceVariant, size: 20),
border: InputBorder.none,
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
@@ -437,9 +420,10 @@ class _UploadButton extends StatelessWidget {
final String title;
final IconData icon;
final VoidCallback onPressed;
final ColorScheme cs;
const _UploadButton(
{required this.title, required this.icon, required this.onPressed});
{required this.title, required this.icon, required this.onPressed, required this.cs});
@override
Widget build(BuildContext context) {
@@ -449,21 +433,21 @@ class _UploadButton extends StatelessWidget {
child: Container(
padding: const EdgeInsets.symmetric(vertical: 20),
decoration: BoxDecoration(
color: const Color(0xFF1A1F3A),
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0xFF6366F1).withOpacity(0.3),
color: cs.primary.withValues(alpha: 0.3),
style: BorderStyle.solid),
),
child: Column(
children: [
Icon(icon, color: const Color(0xFF6366F1), size: 30),
Icon(icon, color: cs.primary, size: 30),
const SizedBox(height: 8),
Text(title,
style: const TextStyle(color: Colors.white70, fontSize: 12)),
style: TextStyle(color: cs.onSurface.withValues(alpha: 0.7), fontSize: 12)),
const SizedBox(height: 4),
const Text("اضغط للرفع",
style: TextStyle(color: Colors.white38, fontSize: 10)),
Text("اضغط للرفع",
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 10)),
],
),
),
@@ -471,22 +455,21 @@ class _UploadButton extends StatelessWidget {
}
}
// === شاشة التفاصيل ===
class EmployeeDetails extends StatelessWidget {
final int index;
const EmployeeDetails({super.key, required this.index});
@override
Widget build(BuildContext context) {
const Color bgColor = Color(0xFF0A0E27);
final cs = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: bgColor,
backgroundColor: cs.surface,
appBar: AppBar(
title:
const Text('تفاصيل الموظف', style: TextStyle(color: Colors.white)),
backgroundColor: bgColor,
iconTheme: const IconThemeData(color: Colors.white),
Text('تفاصيل الموظف', style: TextStyle(color: cs.onSurface)),
backgroundColor: cs.surface,
iconTheme: IconThemeData(color: cs.onSurface),
elevation: 0,
),
body: GetBuilder<EmployeeController>(
@@ -497,24 +480,26 @@ class EmployeeDetails extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("الهوية الأمامية",
Text("الهوية الأمامية",
style: TextStyle(
color: Colors.white,
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.bold)),
const SizedBox(height: 10),
_buildImageViewer(
'${AppLink.server}/card_image/idFrontEmployee-$employeeId.jpg',
cs,
),
const SizedBox(height: 30),
const Text("الهوية الخلفية",
Text("الهوية الخلفية",
style: TextStyle(
color: Colors.white,
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.bold)),
const SizedBox(height: 10),
_buildImageViewer(
'${AppLink.server}/card_image/idbackEmployee-$employeeId.jpg',
cs,
),
],
),
@@ -524,14 +509,14 @@ class EmployeeDetails extends StatelessWidget {
);
}
Widget _buildImageViewer(String url) {
Widget _buildImageViewer(String url, ColorScheme cs) {
return Container(
width: double.infinity,
height: 220,
decoration: BoxDecoration(
color: const Color(0xFF1A1F3A),
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.white.withOpacity(0.1)),
border: Border.all(color: cs.outline.withValues(alpha: 0.1)),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
@@ -539,23 +524,23 @@ class EmployeeDetails extends StatelessWidget {
url,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return const Center(
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.broken_image_rounded,
color: Colors.white24, size: 50),
SizedBox(height: 8),
color: cs.onSurface.withValues(alpha: 0.24), size: 50),
const SizedBox(height: 8),
Text("فشل تحميل الصورة",
style: TextStyle(color: Colors.white24)),
style: TextStyle(color: cs.onSurface.withValues(alpha: 0.24))),
],
),
);
},
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return const Center(
child: CircularProgressIndicator(color: Color(0xFF6366F1)));
return Center(
child: CircularProgressIndicator(color: cs.primary));
},
),
),
@@ -126,47 +126,51 @@ class _ErrorListPageState extends State<ErrorListPage> {
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: const Color(0xFFF0F4F8),
appBar: _buildAppBar(),
backgroundColor: cs.surface,
appBar: _buildAppBar(cs),
body: Column(
children: [
_SearchBar(
controller: _phoneCtrl,
onSearch: _searchByPhone,
onClear: _clearSearch,
cs: cs,
),
Expanded(
child: _buildBody(),
child: _buildBody(cs),
),
],
),
);
}
PreferredSizeWidget _buildAppBar() {
PreferredSizeWidget _buildAppBar(ColorScheme cs) {
return AppBar(
elevation: 0,
backgroundColor: const Color(0xFF0F172A),
foregroundColor: Colors.white,
backgroundColor: cs.surfaceContainerHighest,
foregroundColor: cs.onSurface,
centerTitle: true,
title: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
color: cs.onSurface.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white.withOpacity(0.2)),
border: Border.all(color: cs.onSurface.withValues(alpha: 0.2)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline, color: Colors.red, size: 22),
Icon(Icons.error_outline, color: cs.error, size: 22),
const SizedBox(width: 8),
const Text(
Text(
"سجل الأخطاء",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: cs.onSurface,
),
),
],
@@ -175,11 +179,11 @@ class _ErrorListPageState extends State<ErrorListPage> {
);
}
Widget _buildBody() {
Widget _buildBody(ColorScheme cs) {
if (_loading) {
return const Center(
return Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF0F172A)),
valueColor: AlwaysStoppedAnimation<Color>(cs.primary),
),
);
}
@@ -189,13 +193,13 @@ class _ErrorListPageState extends State<ErrorListPage> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 64, color: Colors.red.shade300),
Icon(Icons.error_outline, size: 64, color: cs.error),
const SizedBox(height: 16),
Text(
_errorMsg!,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.red.shade700,
color: cs.error,
fontSize: 14,
),
),
@@ -209,15 +213,15 @@ class _ErrorListPageState extends State<ErrorListPage> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
const Icon(
Icons.check_circle_outline,
size: 64,
color: Colors.green.shade300,
color: Color(0xFF10B981),
),
const SizedBox(height: 16),
const Text(
Text(
'لا توجد سجلات أخطاء',
style: TextStyle(fontSize: 16, color: Colors.grey),
style: TextStyle(fontSize: 16, color: cs.onSurfaceVariant),
),
],
),
@@ -232,12 +236,12 @@ class _ErrorListPageState extends State<ErrorListPage> {
await _fetchLast20();
}
},
color: const Color(0xFF0F172A),
color: cs.primary,
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
itemCount: _items.length,
itemBuilder: (context, index) {
return _ErrorTile(_items[index], index);
return _ErrorTile(_items[index], index, cs);
},
),
);
@@ -254,11 +258,13 @@ class _SearchBar extends StatefulWidget {
final TextEditingController controller;
final VoidCallback onSearch;
final VoidCallback onClear;
final ColorScheme cs;
const _SearchBar({
required this.controller,
required this.onSearch,
required this.onClear,
required this.cs,
});
@override
@@ -270,18 +276,16 @@ class _SearchBarState extends State<_SearchBar> {
@override
Widget build(BuildContext context) {
final cs = widget.cs;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.white, Colors.grey.shade50],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
color: Colors.black.withValues(alpha: 0.08),
blurRadius: 16,
spreadRadius: 2,
)
@@ -303,15 +307,13 @@ class _SearchBarState extends State<_SearchBar> {
keyboardType: TextInputType.phone,
textDirection: TextDirection.rtl,
onSubmitted: (_) => widget.onSearch(),
style: const TextStyle(fontSize: 15),
style: TextStyle(fontSize: 15, color: cs.onSurface),
decoration: InputDecoration(
hintText: 'بحث برقم الهاتف',
hintStyle: TextStyle(color: Colors.grey.shade400),
hintStyle: TextStyle(color: cs.onSurfaceVariant),
prefixIcon: Icon(
Icons.search,
color: _isFocused
? const Color(0xFF0F172A)
: Colors.grey.shade400,
color: _isFocused ? cs.primary : cs.onSurfaceVariant,
),
suffixIcon: widget.controller.text.isNotEmpty
? InkWell(
@@ -320,28 +322,28 @@ class _SearchBarState extends State<_SearchBar> {
setState(() {});
},
child: Icon(Icons.close,
color: Colors.grey.shade400, size: 20),
color: cs.onSurfaceVariant, size: 20),
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide:
BorderSide(color: Colors.grey.shade300, width: 1),
BorderSide(color: cs.outline, width: 1),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide:
BorderSide(color: Colors.grey.shade300, width: 1),
BorderSide(color: cs.outline, width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: Color(0xFF0F172A),
borderSide: BorderSide(
color: cs.primary,
width: 2,
),
),
filled: true,
fillColor: Colors.white,
fillColor: cs.surfaceContainerHighest,
contentPadding: const EdgeInsets.symmetric(vertical: 12),
),
onChanged: (_) => setState(() {}),
@@ -352,8 +354,8 @@ class _SearchBarState extends State<_SearchBar> {
ElevatedButton(
onPressed: widget.onSearch,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0F172A),
foregroundColor: Colors.white,
backgroundColor: cs.primary,
foregroundColor: cs.onPrimary,
padding: const EdgeInsets.symmetric(horizontal: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
@@ -372,9 +374,9 @@ class _SearchBarState extends State<_SearchBar> {
OutlinedButton(
onPressed: widget.onClear,
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFF0F172A),
side: const BorderSide(
color: Color(0xFF0F172A),
foregroundColor: cs.primary,
side: BorderSide(
color: cs.primary,
width: 1.5,
),
shape: RoundedRectangleBorder(
@@ -395,12 +397,12 @@ class _SearchBarState extends State<_SearchBar> {
class _ErrorTile extends StatelessWidget {
final ErrorLog item;
final int index;
final ColorScheme cs;
const _ErrorTile(this.item, this.index);
const _ErrorTile(this.item, this.index, this.cs);
@override
Widget build(BuildContext context) {
// تحديد الألوان والأيقونات بناءً على نوع المستخدم
final isDriver = item.userType.toLowerCase().contains('driver') ||
item.userType.toLowerCase().contains('سائق');
@@ -410,22 +412,18 @@ class _ErrorTile extends StatelessWidget {
final userTypeLabel = isDriver ? "سائق" : "راكب";
final userTypeBgColor = isDriver
? const Color(0xFF10B981).withOpacity(0.1)
: const Color(0xFFF59E0B).withOpacity(0.1);
? const Color(0xFF10B981).withValues(alpha: 0.1)
: const Color(0xFFF59E0B).withValues(alpha: 0.1);
return Container(
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.white, Colors.grey.shade50],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey.shade200, width: 1),
border: Border.all(color: cs.outline, width: 1),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.06),
color: Colors.black.withValues(alpha: 0.06),
blurRadius: 12,
spreadRadius: 1,
)
@@ -435,7 +433,6 @@ class _ErrorTile extends StatelessWidget {
borderRadius: BorderRadius.circular(16),
child: Stack(
children: [
// خط علوي ملون
Positioned(
top: 0,
left: 0,
@@ -446,7 +443,7 @@ class _ErrorTile extends StatelessWidget {
gradient: LinearGradient(
colors: [
const Color(0xFFEF4444),
Colors.red.shade400,
cs.error,
],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
@@ -459,7 +456,6 @@ class _ErrorTile extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
// الصف الأول: رقم الخطأ ونوع المستخدم
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -493,7 +489,7 @@ class _ErrorTile extends StatelessWidget {
'#${item.id}',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade500,
color: cs.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
@@ -501,14 +497,13 @@ class _ErrorTile extends StatelessWidget {
),
const SizedBox(height: 12),
// عنوان الخطأ (قابل للنسخ)
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.red.shade50,
color: cs.error.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Colors.red.shade200,
color: cs.error.withValues(alpha: 0.3),
width: 1,
),
),
@@ -516,7 +511,7 @@ class _ErrorTile extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.warning_amber_rounded,
size: 18, color: Colors.red.shade600),
size: 18, color: cs.error),
const SizedBox(width: 8),
Expanded(
child: SelectableText(
@@ -524,7 +519,7 @@ class _ErrorTile extends StatelessWidget {
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: Colors.red.shade900,
color: cs.error,
height: 1.4,
),
),
@@ -534,15 +529,14 @@ class _ErrorTile extends StatelessWidget {
),
const SizedBox(height: 12),
// التفاصيل (إن وجدت)
if (item.details.isNotEmpty) ...[
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade50,
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Colors.grey.shade200,
color: cs.outline,
width: 1,
),
),
@@ -550,14 +544,14 @@ class _ErrorTile extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.info_outline,
size: 16, color: Colors.grey.shade600),
size: 16, color: cs.onSurfaceVariant),
const SizedBox(width: 8),
Expanded(
child: SelectableText(
item.details,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade700,
color: cs.onSurfaceVariant,
height: 1.4,
),
),
@@ -568,7 +562,6 @@ class _ErrorTile extends StatelessWidget {
const SizedBox(height: 12),
],
// معلومات تقنية
Wrap(
spacing: 6,
runSpacing: 6,
@@ -578,25 +571,29 @@ class _ErrorTile extends StatelessWidget {
icon: Icons.phone,
label: 'الهاتف',
value: item.phone,
color: Colors.blue,
color: cs.primary,
cs: cs,
),
_buildInfoBadge(
icon: Icons.person_outline,
label: 'المعرف',
value: item.userId,
color: Colors.purple,
cs: cs,
),
_buildInfoBadge(
icon: Icons.devices,
label: 'Path',
value: item.device,
color: Colors.orange,
cs: cs,
),
_buildInfoBadge(
icon: Icons.schedule,
label: 'التاريخ',
value: item.createdAt,
color: Colors.teal,
cs: cs,
),
],
),
@@ -614,13 +611,14 @@ class _ErrorTile extends StatelessWidget {
required String label,
required String value,
required Color color,
required ColorScheme cs,
}) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: color.withOpacity(0.08),
color: color.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withOpacity(0.3), width: 1),
border: Border.all(color: color.withValues(alpha: 0.3), width: 1),
),
child: Row(
mainAxisSize: MainAxisSize.min,
@@ -635,7 +633,7 @@ class _ErrorTile extends StatelessWidget {
text: "$label: ",
style: TextStyle(
fontSize: 10,
color: color.withOpacity(0.7),
color: color.withValues(alpha: 0.7),
fontWeight: FontWeight.w500,
),
),
@@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_admin/constant/colors.dart';
import 'package:siro_admin/controller/admin/financial_v2_controller.dart';
class FinancialV2Page extends StatelessWidget {
@@ -8,19 +7,21 @@ class FinancialV2Page extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final controller = Get.put(FinancialV2Controller());
return Scaffold(
backgroundColor: AppColor.bg,
backgroundColor: cs.surface,
appBar: AppBar(
title: const Text('الإدارة المالية المتقدمة',
style: TextStyle(fontWeight: FontWeight.bold)),
backgroundColor: AppColor.surface,
title: Text('الإدارة المالية المتقدمة',
style: TextStyle(fontWeight: FontWeight.bold, color: cs.onSurface)),
backgroundColor: cs.surface,
elevation: 0,
centerTitle: true,
iconTheme: IconThemeData(color: cs.onSurface),
actions: [
IconButton(
icon: const Icon(Icons.refresh_rounded),
icon: Icon(Icons.refresh_rounded, color: cs.onSurface),
onPressed: () => controller.fetchAllFinancials(),
)
],
@@ -28,8 +29,8 @@ class FinancialV2Page extends StatelessWidget {
body: GetBuilder<FinancialV2Controller>(
builder: (ctrl) {
if (ctrl.isLoading) {
return const Center(
child: CircularProgressIndicator(color: AppColor.accent));
return Center(
child: CircularProgressIndicator(color: cs.primary));
}
return SingleChildScrollView(
@@ -37,13 +38,13 @@ class FinancialV2Page extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildMainFinancialStats(ctrl.stats),
_buildMainFinancialStats(ctrl.stats, cs),
const SizedBox(height: 24),
_buildSectionTitle('طرق الدفع'),
_buildPaymentMethodBreakdown(ctrl.stats),
_buildSectionTitle('طرق الدفع', cs),
_buildPaymentMethodBreakdown(ctrl.stats, cs),
const SizedBox(height: 32),
_buildSectionTitle('تسويات الكباتن (مستحقات معلقة)'),
_buildSettlementsList(ctrl.settlements),
_buildSectionTitle('تسويات الكباتن (مستحقات معلقة)', cs),
_buildSettlementsList(ctrl.settlements, cs),
const SizedBox(height: 40),
],
),
@@ -53,13 +54,13 @@ class FinancialV2Page extends StatelessWidget {
);
}
Widget _buildSectionTitle(String title) {
Widget _buildSectionTitle(String title, ColorScheme cs) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Text(
title,
style: const TextStyle(
color: AppColor.textPrimary,
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.bold,
),
@@ -67,14 +68,15 @@ class FinancialV2Page extends StatelessWidget {
);
}
Widget _buildMainFinancialStats(Map<String, dynamic> stats) {
Widget _buildMainFinancialStats(Map<String, dynamic> stats, ColorScheme cs) {
return Column(
children: [
_buildFinancialCard(
'إجمالي عمولة المنصة',
'${stats['total_platform_commission'] ?? 0} ج.م',
Icons.account_balance_rounded,
AppColor.accent,
cs.primary,
cs,
),
const SizedBox(height: 12),
Row(
@@ -84,7 +86,8 @@ class FinancialV2Page extends StatelessWidget {
'إجمالي دخل الكباتن',
'${stats['total_driver_pay'] ?? 0}',
Icons.person_pin_rounded,
AppColor.info,
cs.tertiary,
cs,
isSmall: true,
),
),
@@ -94,7 +97,8 @@ class FinancialV2Page extends StatelessWidget {
'إجمالي الإيرادات',
'${stats['total_revenue'] ?? 0}',
Icons.payments_rounded,
AppColor.success,
const Color(0xFF10B981),
cs,
isSmall: true,
),
),
@@ -105,18 +109,18 @@ class FinancialV2Page extends StatelessWidget {
}
Widget _buildFinancialCard(
String title, String value, IconData icon, Color color,
String title, String value, IconData icon, Color color, ColorScheme cs,
{bool isSmall = false}) {
return Container(
padding: EdgeInsets.all(isSmall ? 16 : 24),
decoration: BoxDecoration(
color: AppColor.surface,
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: color.withOpacity(0.2)),
border: Border.all(color: color.withValues(alpha: 0.2)),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [AppColor.surface, color.withOpacity(0.05)],
colors: [cs.surfaceContainerHighest, color.withValues(alpha: 0.05)],
),
),
child: Row(
@@ -124,7 +128,7 @@ class FinancialV2Page extends StatelessWidget {
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(14),
),
child: Icon(icon, color: color, size: isSmall ? 20 : 28),
@@ -135,12 +139,12 @@ class FinancialV2Page extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: const TextStyle(
color: AppColor.textSecondary, fontSize: 12)),
style: TextStyle(
color: cs.onSurfaceVariant, fontSize: 12)),
const SizedBox(height: 4),
Text(value,
style: TextStyle(
color: AppColor.textPrimary,
color: cs.onSurface,
fontSize: isSmall ? 18 : 24,
fontWeight: FontWeight.bold)),
],
@@ -151,7 +155,7 @@ class FinancialV2Page extends StatelessWidget {
);
}
Widget _buildPaymentMethodBreakdown(Map<String, dynamic> stats) {
Widget _buildPaymentMethodBreakdown(Map<String, dynamic> stats, ColorScheme cs) {
double cash = double.tryParse(stats['cash_payments'].toString()) ?? 0;
double digital = double.tryParse(stats['digital_payments'].toString()) ?? 0;
double total = cash + digital;
@@ -160,21 +164,21 @@ class FinancialV2Page extends StatelessWidget {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColor.surface,
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(20),
),
child: Column(
children: [
_buildPaymentBar('نقدي (Cash)', cash, total, AppColor.warning),
_buildPaymentBar('نقدي (Cash)', cash, total, cs.tertiary, cs),
const SizedBox(height: 16),
_buildPaymentBar('إلكتروني / محفظة', digital, total, AppColor.info),
_buildPaymentBar('إلكتروني / محفظة', digital, total, cs.tertiary, cs),
],
),
);
}
Widget _buildPaymentBar(
String label, double value, double total, Color color) {
String label, double value, double total, Color color, ColorScheme cs) {
double percent = value / total;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -183,8 +187,7 @@ class FinancialV2Page extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label,
style:
const TextStyle(color: AppColor.textPrimary, fontSize: 13)),
style: TextStyle(color: cs.onSurface, fontSize: 13)),
Text('${value.toStringAsFixed(0)} ج.م',
style: TextStyle(color: color, fontWeight: FontWeight.bold)),
],
@@ -194,7 +197,7 @@ class FinancialV2Page extends StatelessWidget {
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: percent,
backgroundColor: AppColor.divider,
backgroundColor: cs.outline,
color: color,
minHeight: 8,
),
@@ -203,9 +206,11 @@ class FinancialV2Page extends StatelessWidget {
);
}
Widget _buildSettlementsList(List<dynamic> settlements) {
Widget _buildSettlementsList(List<dynamic> settlements, ColorScheme cs) {
if (settlements.isEmpty) {
return const Center(child: Text('لا توجد تسويات معلقة'));
return Center(
child: Text('لا توجد تسويات معلقة',
style: TextStyle(color: cs.onSurfaceVariant)));
}
return ListView.builder(
@@ -218,9 +223,9 @@ class FinancialV2Page extends StatelessWidget {
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColor.surface,
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColor.divider),
border: Border.all(color: cs.outline),
),
child: Row(
children: [
@@ -229,36 +234,36 @@ class FinancialV2Page extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${s['first_name']} ${s['last_name']}',
style: const TextStyle(
color: AppColor.textPrimary,
style: TextStyle(
color: cs.onSurface,
fontWeight: FontWeight.bold)),
Text(s['phone'] ?? '',
style: const TextStyle(
color: AppColor.textSecondary, fontSize: 12)),
style: TextStyle(
color: cs.onSurfaceVariant, fontSize: 12)),
const SizedBox(height: 4),
Text('${s['total_rides']} رحلة مكتملة',
style: const TextStyle(
color: AppColor.info, fontSize: 11)),
style: TextStyle(
color: cs.tertiary, fontSize: 11)),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
const Text('المستحقات',
Text('المستحقات',
style: TextStyle(
color: AppColor.textSecondary, fontSize: 10)),
color: cs.onSurfaceVariant, fontSize: 10)),
Text('${s['total_earned']} ج.م',
style: const TextStyle(
color: AppColor.accent,
style: TextStyle(
color: cs.primary,
fontSize: 18,
fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
backgroundColor: AppColor.accent.withOpacity(0.1),
foregroundColor: AppColor.accent,
backgroundColor: cs.primary.withValues(alpha: 0.1),
foregroundColor: cs.primary,
elevation: 0,
minimumSize: const Size(80, 32),
shape: RoundedRectangleBorder(
@@ -15,16 +15,14 @@ class _HeatmapPageState extends State<HeatmapPage> {
List<CircleMarker> _markers = [];
bool _isLoading = true;
// ─── فلاتر ───
String _selectedSource = 'all'; // all | geofence | app_usage | silent_push
String _selectedCountry = 'all'; // all | JO | SY | EG | IQ
String _selectedSource = 'all';
String _selectedCountry = 'all';
int _daysFilter = 7;
final MapController _mapController = MapController();
// مراكز كل دولة للانتقال السريع
final Map<String, LatLng> _countryCenters = {
'all': const LatLng(31.9522, 35.9334), // عمان
'all': const LatLng(31.9522, 35.9334),
'JO': const LatLng(31.9522, 35.9334),
'SY': const LatLng(33.5138, 36.2765),
'EG': const LatLng(30.0444, 31.2357),
@@ -60,18 +58,17 @@ class _HeatmapPageState extends State<HeatmapPage> {
final lng = double.tryParse(point['longitude'].toString()) ?? 0.0;
final source = point['source'].toString();
// ألوان حسب المصدر
Color markerColor;
switch (source) {
case 'geofence':
markerColor = Colors.green.withOpacity(0.65); // 🟢 دخل منطقة
markerColor = Colors.green.withValues(alpha: 0.65);
break;
case 'silent_push':
markerColor = Colors.orange.withOpacity(0.55); // 🟠 إيقاظ صامت
markerColor = Colors.orange.withValues(alpha: 0.55);
break;
case 'app_usage':
default:
markerColor = Colors.blue.withOpacity(0.5); // 🔵 فتح عادي
markerColor = Colors.blue.withValues(alpha: 0.5);
}
return CircleMarker(
@@ -93,22 +90,23 @@ class _HeatmapPageState extends State<HeatmapPage> {
void _applyFilter() {
_fetchHeatmapData();
// تحريك الخريطة لمركز الدولة المختارة
final center = _countryCenters[_selectedCountry] ?? _countryCenters['all']!;
_mapController.move(center, 11.0);
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: const Color(0xFF0F0F1A),
backgroundColor: cs.surface,
appBar: AppBar(
title: const Text(
'خريطة النشاط الحرارية',
style: TextStyle(fontWeight: FontWeight.w700, fontSize: 15),
),
backgroundColor: const Color(0xFF1A1A2E),
foregroundColor: Colors.white,
backgroundColor: cs.surface,
foregroundColor: cs.onSurface,
centerTitle: true,
actions: [
IconButton(
@@ -120,14 +118,13 @@ class _HeatmapPageState extends State<HeatmapPage> {
),
body: Column(
children: [
// ─── شريط الفلاتر ───
Container(
color: const Color(0xFF1A1A2E),
color: cs.surfaceContainerHighest,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Column(
children: [
// فلتر المصدر
_buildFilterRow(
cs: cs,
label: 'المصدر:',
options: const {
'all': 'الكل',
@@ -139,8 +136,8 @@ class _HeatmapPageState extends State<HeatmapPage> {
onChanged: (v) => setState(() => _selectedSource = v),
),
const SizedBox(height: 6),
// فلتر الدولة
_buildFilterRow(
cs: cs,
label: 'الدولة:',
options: const {
'all': 'الكل',
@@ -153,10 +150,9 @@ class _HeatmapPageState extends State<HeatmapPage> {
onChanged: (v) => setState(() => _selectedCountry = v),
),
const SizedBox(height: 6),
// فلتر الفترة الزمنية + زر تطبيق
Row(
children: [
const Text('الفترة:', style: TextStyle(color: Colors.white54, fontSize: 11, fontWeight: FontWeight.bold)),
Text('الفترة:', style: TextStyle(color: cs.onSurfaceVariant, fontSize: 11, fontWeight: FontWeight.bold)),
const SizedBox(width: 8),
...[1, 7, 30].map((days) => Padding(
padding: const EdgeInsets.only(right: 6),
@@ -166,19 +162,19 @@ class _HeatmapPageState extends State<HeatmapPage> {
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: _daysFilter == days
? const Color(0xFF6366F1)
: const Color(0xFF2D2D42),
? cs.primary
: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: _daysFilter == days
? const Color(0xFF818CF8)
? cs.primary
: Colors.transparent,
),
),
child: Text(
'$days يوم',
style: TextStyle(
color: _daysFilter == days ? Colors.white : Colors.white54,
color: _daysFilter == days ? cs.onSurface : cs.onSurfaceVariant,
fontSize: 10,
fontWeight: FontWeight.bold,
),
@@ -192,8 +188,8 @@ class _HeatmapPageState extends State<HeatmapPage> {
icon: const Icon(Icons.filter_alt_rounded, size: 14),
label: const Text('تطبيق', style: TextStyle(fontSize: 11)),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF6366F1),
foregroundColor: Colors.white,
backgroundColor: cs.primary,
foregroundColor: cs.onPrimary,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
),
@@ -204,18 +200,17 @@ class _HeatmapPageState extends State<HeatmapPage> {
),
),
// ─── الخريطة ───
Expanded(
child: Stack(
children: [
_isLoading
? const Center(
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(color: Color(0xFF6366F1)),
SizedBox(height: 12),
Text('جاري تحميل البيانات...', style: TextStyle(color: Colors.white54, fontSize: 12)),
CircularProgressIndicator(color: cs.primary),
const SizedBox(height: 12),
Text('جاري تحميل البيانات...', style: TextStyle(color: cs.onSurfaceVariant, fontSize: 12)),
],
),
)
@@ -234,44 +229,42 @@ class _HeatmapPageState extends State<HeatmapPage> {
],
),
// ─── مفتاح الألوان ───
Positioned(
bottom: 12,
right: 12,
child: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: const Color(0xFF1A1A2E).withOpacity(0.9),
color: cs.surfaceContainerHighest.withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.white12),
border: Border.all(color: cs.outline.withValues(alpha: 0.3)),
),
child: const Column(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_LegendItem(color: Colors.green, label: 'دخل منطقة سياج'),
SizedBox(height: 4),
_LegendItem(color: Colors.blue, label: 'فتح عادي للتطبيق'),
SizedBox(height: 4),
_LegendItem(color: Colors.orange, label: 'إيقاظ صامت'),
_LegendItem(color: Colors.green, label: 'دخل منطقة سياج', cs: cs),
const SizedBox(height: 4),
_LegendItem(color: Colors.blue, label: 'فتح عادي للتطبيق', cs: cs),
const SizedBox(height: 4),
_LegendItem(color: Colors.orange, label: 'إيقاظ صامت', cs: cs),
],
),
),
),
// ─── عدد النقاط ───
Positioned(
top: 12,
left: 12,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFF1A1A2E).withOpacity(0.9),
color: cs.surfaceContainerHighest.withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.white12),
border: Border.all(color: cs.outline.withValues(alpha: 0.3)),
),
child: Text(
'${_markers.length} نقطة',
style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold),
style: TextStyle(color: cs.onSurface, fontSize: 11, fontWeight: FontWeight.bold),
),
),
),
@@ -284,6 +277,7 @@ class _HeatmapPageState extends State<HeatmapPage> {
}
Widget _buildFilterRow({
required ColorScheme cs,
required String label,
required Map<String, String> options,
required String selected,
@@ -291,7 +285,7 @@ class _HeatmapPageState extends State<HeatmapPage> {
}) {
return Row(
children: [
Text(label, style: const TextStyle(color: Colors.white54, fontSize: 11, fontWeight: FontWeight.bold)),
Text(label, style: TextStyle(color: cs.onSurfaceVariant, fontSize: 11, fontWeight: FontWeight.bold)),
const SizedBox(width: 8),
Expanded(
child: SingleChildScrollView(
@@ -306,16 +300,16 @@ class _HeatmapPageState extends State<HeatmapPage> {
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF6366F1) : const Color(0xFF2D2D42),
color: isSelected ? cs.primary : cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected ? const Color(0xFF818CF8) : Colors.transparent,
color: isSelected ? cs.primary : Colors.transparent,
),
),
child: Text(
e.value,
style: TextStyle(
color: isSelected ? Colors.white : Colors.white54,
color: isSelected ? cs.onSurface : cs.onSurfaceVariant,
fontSize: 10,
fontWeight: FontWeight.bold,
),
@@ -335,7 +329,8 @@ class _HeatmapPageState extends State<HeatmapPage> {
class _LegendItem extends StatelessWidget {
final Color color;
final String label;
const _LegendItem({required this.color, required this.label});
final ColorScheme cs;
const _LegendItem({required this.color, required this.label, required this.cs});
@override
Widget build(BuildContext context) {
@@ -344,7 +339,7 @@ class _LegendItem extends StatelessWidget {
children: [
Container(width: 10, height: 10, decoration: BoxDecoration(color: color, shape: BoxShape.circle)),
const SizedBox(width: 6),
Text(label, style: const TextStyle(color: Colors.white70, fontSize: 10)),
Text(label, style: TextStyle(color: cs.onSurfaceVariant, fontSize: 10)),
],
);
}
File diff suppressed because it is too large Load Diff
@@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:http/http.dart' as http;
import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart';
import '../../../constant/colors.dart';
import '../../../constant/links.dart';
class SocialIntelligenceController extends GetxController {
@@ -42,36 +41,38 @@ class SocialIntelligenceScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final controller = Get.put(SocialIntelligenceController());
return Scaffold(
backgroundColor: AppColor.bg,
backgroundColor: cs.surface,
appBar: AppBar(
title: const Text(
title: Text(
'استخبارات السوشيال ميديا (Social Intelligence)',
style: TextStyle(fontWeight: FontWeight.w700, fontSize: 16),
style: TextStyle(fontWeight: FontWeight.w700, fontSize: 16, color: cs.onSurface),
),
backgroundColor: AppColor.bg,
backgroundColor: cs.surface,
elevation: 0,
foregroundColor: AppColor.textPrimary,
foregroundColor: cs.onSurface,
iconTheme: IconThemeData(color: cs.onSurface),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
icon: Icon(Icons.refresh, color: cs.onSurface),
onPressed: () => controller.fetchReports(),
)
],
),
body: Obx(() {
if (controller.isLoading.value) {
return const Center(
child: CircularProgressIndicator(color: AppColor.accent));
return Center(
child: CircularProgressIndicator(color: cs.primary));
}
if (controller.reports.isEmpty) {
return const Center(
return Center(
child: Text(
'لا توجد تقارير حالياً',
style: TextStyle(color: AppColor.textSecondary),
style: TextStyle(color: cs.onSurfaceVariant),
),
);
}
@@ -84,45 +85,45 @@ class SocialIntelligenceScreen extends StatelessWidget {
final isWeekly = report['is_weekly'].toString() == '1';
return Card(
color: AppColor.surface,
color: cs.surfaceContainerHighest,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(
color: isWeekly ? AppColor.accent : AppColor.divider,
color: isWeekly ? cs.primary : cs.outline,
width: isWeekly ? 1.5 : 1.0),
),
margin: const EdgeInsets.only(bottom: 16),
child: ExpansionTile(
iconColor: AppColor.accent,
collapsedIconColor: AppColor.textSecondary,
iconColor: cs.primary,
collapsedIconColor: cs.onSurfaceVariant,
title: Text(
isWeekly
? '📈 تقرير السوق الأسبوعي الشامل'
: '📊 تقرير ${report['platform'].toString().toUpperCase()}',
style: TextStyle(
fontWeight: FontWeight.bold,
color: isWeekly ? AppColor.accent : AppColor.textPrimary,
color: isWeekly ? cs.primary : cs.onSurface,
fontSize: 14,
),
),
subtitle: Text(
report['created_at'].toString(),
style: const TextStyle(
color: AppColor.textSecondary, fontSize: 11),
style: TextStyle(
color: cs.onSurfaceVariant, fontSize: 11),
),
children: [
Container(
padding: const EdgeInsets.all(16),
width: double.infinity,
decoration: const BoxDecoration(
border: Border(top: BorderSide(color: AppColor.divider)),
decoration: BoxDecoration(
border: Border(top: BorderSide(color: cs.outline)),
),
child: Directionality(
textDirection: TextDirection.rtl,
child: HtmlWidget(
report['report_html'] ?? '',
textStyle: const TextStyle(
color: AppColor.textPrimary,
textStyle: TextStyle(
color: cs.onSurface,
fontSize: 13,
height: 1.6),
),
+89 -126
View File
@@ -7,19 +7,6 @@ import 'package:siro_admin/views/widgets/snackbar.dart';
import '../../print.dart';
// ══════════════════════════════════════════════════════════════
// DESIGN TOKENS (same as AdminHomePage)
// ══════════════════════════════════════════════════════════════
const Color _bg = Color(0xFF0D1117);
const Color _surface = Color(0xFF161B22);
const Color _surfaceElevated = Color(0xFF1C2333);
const Color _accent = Color(0xFF00D4AA);
const Color _warning = Color(0xFFFFCB6B);
const Color _info = Color(0xFF82AAFF);
const Color _textPrimary = Color(0xFFE6EDF3);
const Color _textSecondary = Color(0xFF7D8590);
const Color _divider = Color(0xFF21262D);
class PackageUpdateScreen extends StatelessWidget {
PackageUpdateScreen({super.key});
@@ -27,19 +14,20 @@ class PackageUpdateScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: _bg,
appBar: _buildAppBar(),
backgroundColor: cs.surface,
appBar: _buildAppBar(cs),
body: GetBuilder<PackageController>(
builder: (controller) {
if (controller.isLoading.value) {
return const Center(
child: CircularProgressIndicator(color: _accent, strokeWidth: 2),
return Center(
child: CircularProgressIndicator(color: cs.primary, strokeWidth: 2),
);
}
if (controller.packages.isEmpty) {
return _buildEmptyState();
return _buildEmptyState(cs);
}
return ListView.separated(
@@ -48,7 +36,7 @@ class PackageUpdateScreen extends StatelessWidget {
separatorBuilder: (_, __) => const SizedBox(height: 10),
itemBuilder: (context, index) {
final package = controller.packages[index];
return _buildPackageCard(context, package, controller);
return _buildPackageCard(context, package, controller, cs);
},
);
},
@@ -56,10 +44,9 @@ class PackageUpdateScreen extends StatelessWidget {
);
}
// ─────────────────────────── APP BAR ───────────────────────────
PreferredSizeWidget _buildAppBar() {
PreferredSizeWidget _buildAppBar(ColorScheme cs) {
return AppBar(
backgroundColor: _bg,
backgroundColor: cs.surface,
elevation: 0,
surfaceTintColor: Colors.transparent,
leading: GestureDetector(
@@ -67,12 +54,12 @@ class PackageUpdateScreen extends StatelessWidget {
child: Container(
margin: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: _surface,
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: _divider),
border: Border.all(color: cs.outline),
),
child: const Icon(Icons.arrow_back_ios_new_rounded,
color: _textSecondary, size: 16),
child: Icon(Icons.arrow_back_ios_new_rounded,
color: cs.onSurfaceVariant, size: 16),
),
),
title: Row(
@@ -80,18 +67,18 @@ class PackageUpdateScreen extends StatelessWidget {
Container(
padding: const EdgeInsets.all(7),
decoration: BoxDecoration(
color: _accent.withOpacity(0.12),
color: cs.primary.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(9),
border: Border.all(color: _accent.withOpacity(0.25)),
border: Border.all(color: cs.primary.withValues(alpha: 0.25)),
),
child: const Icon(Icons.system_update_rounded,
color: _accent, size: 16),
child: Icon(Icons.system_update_rounded,
color: cs.primary, size: 16),
),
const SizedBox(width: 10),
const Text(
Text(
'تحديث التطبيق',
style: TextStyle(
color: _textPrimary,
color: cs.onSurface,
fontSize: 17,
fontWeight: FontWeight.w600,
),
@@ -105,25 +92,24 @@ class PackageUpdateScreen extends StatelessWidget {
margin: const EdgeInsets.only(right: 16),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: _surface,
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: _divider),
border: Border.all(color: cs.outline),
),
child: const Icon(Icons.refresh_rounded,
color: _textSecondary, size: 18),
child: Icon(Icons.refresh_rounded,
color: cs.onSurfaceVariant, size: 18),
),
),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1),
child: Container(height: 1, color: _divider),
child: Container(height: 1, color: cs.outline),
),
);
}
// ─────────────────────────── PACKAGE CARD ───────────────────────────
Widget _buildPackageCard(
BuildContext context, dynamic package, PackageController controller) {
BuildContext context, dynamic package, PackageController controller, ColorScheme cs) {
final platform = package['platform']?.toString() ?? '';
final isAndroid = platform.toLowerCase().contains('android');
final isIOS = platform.toLowerCase().contains('ios');
@@ -131,8 +117,8 @@ class PackageUpdateScreen extends StatelessWidget {
final Color platformColor = isAndroid
? const Color(0xFF4CAF50)
: isIOS
? _info
: _warning;
? cs.tertiary
: cs.tertiary;
final IconData platformIcon = isAndroid
? Icons.android_rounded
: isIOS
@@ -142,20 +128,19 @@ class PackageUpdateScreen extends StatelessWidget {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _showUpdateDialog(context, package, controller),
onTap: () => _showUpdateDialog(context, package, controller, cs),
borderRadius: BorderRadius.circular(16),
splashColor: _accent.withOpacity(0.06),
splashColor: cs.primary.withValues(alpha: 0.06),
highlightColor: Colors.transparent,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: _surface,
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: _divider),
border: Border.all(color: cs.outline),
),
child: Row(
children: [
// Platform Icon
Container(
width: 48,
height: 48,
@@ -164,27 +149,24 @@ class PackageUpdateScreen extends StatelessWidget {
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
platformColor.withOpacity(0.20),
platformColor.withOpacity(0.06),
platformColor.withValues(alpha: 0.20),
platformColor.withValues(alpha: 0.06),
],
),
borderRadius: BorderRadius.circular(13),
border: Border.all(color: platformColor.withOpacity(0.25)),
border: Border.all(color: platformColor.withValues(alpha: 0.25)),
),
child: Icon(platformIcon, color: platformColor, size: 22),
),
const SizedBox(width: 14),
// Info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
package['appName']?.toString() ?? '—',
style: const TextStyle(
color: _textPrimary,
style: TextStyle(
color: cs.onSurface,
fontSize: 14,
fontWeight: FontWeight.w600,
),
@@ -195,31 +177,29 @@ class PackageUpdateScreen extends StatelessWidget {
_buildTag(platform, platformColor),
const SizedBox(width: 6),
_buildVersionBadge(
package['version']?.toString() ?? '?'),
package['version']?.toString() ?? '?', cs),
],
),
],
),
),
// Update button
Container(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
decoration: BoxDecoration(
color: _accent.withOpacity(0.10),
color: cs.primary.withValues(alpha: 0.10),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: _accent.withOpacity(0.25)),
border: Border.all(color: cs.primary.withValues(alpha: 0.25)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: const [
Icon(Icons.edit_rounded, color: _accent, size: 13),
SizedBox(width: 5),
children: [
Icon(Icons.edit_rounded, color: cs.primary, size: 13),
const SizedBox(width: 5),
Text(
'تعديل',
style: TextStyle(
color: _accent,
color: cs.primary,
fontSize: 11,
fontWeight: FontWeight.w600,
),
@@ -238,7 +218,7 @@ class PackageUpdateScreen extends StatelessWidget {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: color.withOpacity(0.10),
color: color.withValues(alpha: 0.10),
borderRadius: BorderRadius.circular(6),
),
child: Text(
@@ -252,17 +232,17 @@ class PackageUpdateScreen extends StatelessWidget {
);
}
Widget _buildVersionBadge(String version) {
Widget _buildVersionBadge(String version, ColorScheme cs) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: _divider,
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(6),
),
child: Text(
'v$version',
style: const TextStyle(
color: _textSecondary,
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 10,
fontWeight: FontWeight.w500,
fontFamily: 'monospace',
@@ -271,8 +251,7 @@ class PackageUpdateScreen extends StatelessWidget {
);
}
// ─────────────────────────── EMPTY STATE ───────────────────────────
Widget _buildEmptyState() {
Widget _buildEmptyState(ColorScheme cs) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
@@ -280,30 +259,29 @@ class PackageUpdateScreen extends StatelessWidget {
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: _surface,
color: cs.surfaceContainerHighest,
shape: BoxShape.circle,
border: Border.all(color: _divider),
border: Border.all(color: cs.outline),
),
child: const Icon(Icons.inventory_2_outlined,
color: _textSecondary, size: 32),
child: Icon(Icons.inventory_2_outlined,
color: cs.onSurfaceVariant, size: 32),
),
const SizedBox(height: 16),
const Text('لا توجد حزم متاحة',
Text('لا توجد حزم متاحة',
style: TextStyle(
color: _textPrimary,
color: cs.onSurface,
fontSize: 15,
fontWeight: FontWeight.w600)),
const SizedBox(height: 6),
const Text('اسحب للأسفل لإعادة التحميل',
style: TextStyle(color: _textSecondary, fontSize: 12)),
Text('اسحب للأسفل لإعادة التحميل',
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 12)),
],
),
);
}
// ─────────────────────────── UPDATE DIALOG ───────────────────────────
void _showUpdateDialog(
BuildContext context, dynamic package, PackageController controller) {
BuildContext context, dynamic package, PackageController controller, ColorScheme cs) {
controller.versionController.clear();
Get.dialog(
@@ -311,9 +289,9 @@ class PackageUpdateScreen extends StatelessWidget {
backgroundColor: Colors.transparent,
child: Container(
decoration: BoxDecoration(
color: _surfaceElevated,
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(24),
border: Border.all(color: _divider),
border: Border.all(color: cs.outline),
boxShadow: const [
BoxShadow(
color: Colors.black54,
@@ -327,107 +305,95 @@ class PackageUpdateScreen extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: _accent.withOpacity(0.12),
color: cs.primary.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: _accent.withOpacity(0.25)),
border: Border.all(color: cs.primary.withValues(alpha: 0.25)),
),
child: const Icon(Icons.system_update_rounded,
color: _accent, size: 20),
child: Icon(Icons.system_update_rounded,
color: cs.primary, size: 20),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
Text(
'تحديث الإصدار',
style: TextStyle(
color: _textPrimary,
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w700,
),
),
Text(
package['appName']?.toString() ?? '',
style: const TextStyle(
color: _textSecondary, fontSize: 11),
style: TextStyle(
color: cs.onSurfaceVariant, fontSize: 11),
),
],
),
),
],
),
const SizedBox(height: 20),
Container(height: 1, color: _divider),
Container(height: 1, color: cs.outline),
const SizedBox(height: 20),
// Current info
Row(
children: [
_buildInfoChip(Icons.devices_rounded,
package['platform']?.toString() ?? '', _info),
package['platform']?.toString() ?? '', cs.tertiary),
const SizedBox(width: 8),
_buildInfoChip(Icons.tag_rounded,
'الحالي: ${package['version']}', _warning),
'الحالي: ${package['version']}', cs.tertiary),
],
),
const SizedBox(height: 18),
// Input label
const Text(
Text(
'الإصدار الجديد',
style: TextStyle(
color: _textSecondary,
color: cs.onSurfaceVariant,
fontSize: 11,
fontWeight: FontWeight.w600,
letterSpacing: 0.8,
),
),
const SizedBox(height: 8),
// Text input
Container(
decoration: BoxDecoration(
color: _bg,
color: cs.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: _divider),
border: Border.all(color: cs.outline),
),
child: TextField(
controller: controller.versionController,
keyboardType:
const TextInputType.numberWithOptions(decimal: true),
style: const TextStyle(
color: _textPrimary,
style: TextStyle(
color: cs.onSurface,
fontSize: 15,
fontFamily: 'monospace',
fontWeight: FontWeight.w600,
),
decoration: InputDecoration(
hintText: package['version'].toString(),
hintStyle: const TextStyle(
color: _textSecondary,
hintStyle: TextStyle(
color: cs.onSurfaceVariant,
fontFamily: 'monospace',
),
prefixIcon:
const Icon(Icons.tag_rounded, color: _accent, size: 18),
Icon(Icons.tag_rounded, color: cs.primary, size: 18),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(
horizontal: 14, vertical: 14),
),
),
),
const SizedBox(height: 24),
// Actions
Row(
children: [
Expanded(
@@ -437,12 +403,12 @@ class PackageUpdateScreen extends StatelessWidget {
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: const BorderSide(color: _divider),
side: BorderSide(color: cs.outline),
),
),
child: const Text(
child: Text(
'إلغاء',
style: TextStyle(color: _textSecondary, fontSize: 13),
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
),
),
),
@@ -450,11 +416,11 @@ class PackageUpdateScreen extends StatelessWidget {
Expanded(
child: Obx(() => ElevatedButton.icon(
icon: controller.isLoading.value
? const SizedBox(
? SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(
color: Colors.white,
color: cs.onPrimary,
strokeWidth: 2,
),
)
@@ -464,8 +430,8 @@ class PackageUpdateScreen extends StatelessWidget {
style: const TextStyle(fontSize: 13),
),
style: ElevatedButton.styleFrom(
backgroundColor: _accent,
foregroundColor: _bg,
backgroundColor: cs.primary,
foregroundColor: cs.surface,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
@@ -495,9 +461,9 @@ class PackageUpdateScreen extends StatelessWidget {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: color.withOpacity(0.08),
color: color.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withOpacity(0.2)),
border: Border.all(color: color.withValues(alpha: 0.2)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
@@ -518,9 +484,6 @@ class PackageUpdateScreen extends StatelessWidget {
}
}
// ══════════════════════════════════════════════════════════════
// CONTROLLER
// ══════════════════════════════════════════════════════════════
class PackageController extends GetxController {
List packages = [];
var isLoading = false.obs;
@@ -2,14 +2,8 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../constant/box_name.dart';
import '../../../constant/colors.dart';
import '../../../constant/style.dart';
import '../../../controller/admin/passenger_admin_controller.dart';
import '../../../main.dart'; // للوصول إلى box
import '../../widgets/elevated_btn.dart';
import '../../widgets/my_scafold.dart';
import '../../widgets/my_textField.dart';
import '../../widgets/mycircular.dart';
import '../../../main.dart';
import '../../widgets/snackbar.dart';
import 'passenger_details_page.dart';
@@ -21,141 +15,36 @@ class Passengrs extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 1. منطق السوبر أدمن
final cs = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
String myPhone = box.read(BoxName.adminPhone).toString();
bool isSuperAdmin = myPhone == '963942542053' || myPhone == '963992952235';
bool isSuperAdmin =
myPhone == '963942542053' || myPhone == '963992952235';
return MyScafolld(
title: 'Passengers Management'.tr,
isleading: true,
body: [
// استخدام Expanded أو Container بطول الشاشة لتجنب المشاكل
SizedBox(
height: Get.height, // تأمين مساحة العمل
child: GetBuilder<PassengerAdminController>(
builder: (controller) {
if (controller.isLoading) {
return const Center(child: MyCircularProgressIndicator());
}
return Column(
children: [
// --- قسم الإحصائيات والجوائز (Dashboard) ---
Padding(
padding: const EdgeInsets.all(16.0),
child: _buildDashboardCard(context, controller),
),
// --- عنوان القائمة ---
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"All Passengers".tr,
style: AppStyle.title.copyWith(
fontSize: 18, fontWeight: FontWeight.bold),
),
Text(
"${controller.passengersData['message']?.length ?? 0} Users",
style:
TextStyle(color: Colors.grey[600], fontSize: 12),
),
],
),
),
const SizedBox(height: 10),
// --- قائمة الركاب ---
// استخدام Expanded هنا هو الحل الجذري لمكلة Overflow
Expanded(
child: _buildPassengersList(controller, isSuperAdmin),
),
// مساحة سفلية صغيرة لضمان عدم التصاق القائمة بالحافة
const SizedBox(height: 20),
],
);
},
),
),
],
);
}
// --- تصميم بطاقة الإحصائيات (Dashboard) ---
Widget _buildDashboardCard(
BuildContext context, PassengerAdminController controller) {
// جلب العدد بأمان
final String countValue = (controller.passengersData['message'] != null &&
controller.passengersData['message'].isNotEmpty)
? controller.passengersData['message'][0]['countPassenger']
?.toString() ??
'0'
: '0';
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: Column(
return Scaffold(
backgroundColor: cs.surface,
body: Column(
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColor.primaryColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(Icons.groups_rounded,
color: AppColor.primaryColor, size: 30),
),
const SizedBox(width: 15),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Total Passengers'.tr,
style: const TextStyle(fontSize: 14, color: Colors.grey),
),
Text(
countValue,
style: const TextStyle(
fontSize: 24, fontWeight: FontWeight.bold),
),
],
),
],
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
height: 45,
child: ElevatedButton.icon(
icon: const Icon(Icons.card_giftcard,
color: Colors.white, size: 20),
label: Text('Add Prize to Gold Passengers'.tr,
style: const TextStyle(
color: Colors.white, fontWeight: FontWeight.bold)),
style: ElevatedButton.styleFrom(
backgroundColor: AppColor.yellowColor, // لون ذهبي
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
elevation: 0,
),
onPressed: () {
_showAddPrizeDialog(controller);
_buildAppBar(context, cs),
Expanded(
child: GetBuilder<PassengerAdminController>(
builder: (controller) {
if (controller.isLoading) {
return _buildLoadingState(cs);
}
return Column(
children: [
_buildDashboardCard(context, controller, cs),
_buildListHeader(context, controller, cs),
Expanded(
child: _buildPassengersList(
controller, isSuperAdmin, cs),
),
const SizedBox(height: 20),
],
);
},
),
),
@@ -164,20 +53,252 @@ class Passengrs extends StatelessWidget {
);
}
// --- بناء قائمة الركاب ---
Widget _buildAppBar(BuildContext context, ColorScheme cs) {
return Container(
padding: const EdgeInsets.fromLTRB(16, 50, 16, 16),
decoration: BoxDecoration(
color: cs.surface,
border: Border(bottom: BorderSide(color: cs.outline)),
),
child: Row(
children: [
GestureDetector(
onTap: () => Get.back(),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: cs.outline),
),
child: Icon(Icons.arrow_back_ios_new_rounded,
color: cs.onSurfaceVariant, size: 16),
),
),
const SizedBox(width: 12),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: cs.primary.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: cs.primary.withValues(alpha: 0.25)),
),
child: Icon(Icons.groups_rounded, color: cs.primary, size: 18),
),
const SizedBox(width: 10),
Text(
'إدارة الركاب',
style: TextStyle(
color: cs.onSurface,
fontSize: 18,
fontWeight: FontWeight.w700,
),
),
],
),
);
}
Widget _buildLoadingState(ColorScheme cs) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 40,
height: 40,
child: CircularProgressIndicator(
color: cs.primary,
strokeWidth: 2,
backgroundColor: cs.primary.withValues(alpha: 0.1),
),
),
const SizedBox(height: 16),
Text('جاري تحميل الركاب...',
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13)),
],
),
);
}
Widget _buildDashboardCard(
BuildContext context,
PassengerAdminController controller,
ColorScheme cs) {
final String countValue =
(controller.passengersData['message'] != null &&
controller.passengersData['message'].isNotEmpty)
? controller.passengersData['message'][0]['countPassenger']
?.toString() ??
'0'
: '0';
return Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: cs.primary.withValues(alpha: 0.15)),
boxShadow: [
BoxShadow(
color: cs.primary.withValues(alpha: 0.06),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: Column(
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
cs.primary.withValues(alpha: 0.20),
cs.primary.withValues(alpha: 0.08),
],
),
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: cs.primary.withValues(alpha: 0.25)),
),
child: Icon(Icons.groups_rounded,
color: cs.primary, size: 28),
),
const SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'إجمالي الركاب',
style: TextStyle(
color: cs.onSurfaceVariant, fontSize: 12),
),
const SizedBox(height: 4),
Text(
countValue,
style: TextStyle(
color: cs.onSurface,
fontSize: 26,
fontWeight: FontWeight.w800,
height: 1,
),
),
],
),
],
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton.icon(
icon: const Icon(Icons.card_giftcard,
color: Colors.white, size: 18),
label: Text('إضافة جائزة لركاب الذهب',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 13)),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFF59E0B),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14)),
elevation: 0,
),
onPressed: () {
_showAddPrizeDialog(controller, cs);
},
),
),
],
),
),
);
}
Widget _buildListHeader(
BuildContext context,
PassengerAdminController controller,
ColorScheme cs) {
return Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 8),
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.onSurface,
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 10),
Expanded(
child: Container(height: 1, color: cs.outline),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: cs.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: cs.primary.withValues(alpha: 0.2)),
),
child: Text(
'${controller.passengersData['message']?.length ?? 0}',
style: TextStyle(
color: cs.primary,
fontSize: 12,
fontWeight: FontWeight.w700,
),
),
),
],
),
);
}
Widget _buildPassengersList(
PassengerAdminController controller, bool isSuperAdmin) {
final List<dynamic> passengers = controller.passengersData['message'] ?? [];
PassengerAdminController controller,
bool isSuperAdmin,
ColorScheme cs) {
final List<dynamic> passengers =
controller.passengersData['message'] ?? [];
if (passengers.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.person_off_outlined, size: 60, color: Colors.grey[300]),
const SizedBox(height: 10),
Text("No passengers found".tr,
style: TextStyle(color: Colors.grey[400])),
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
shape: BoxShape.circle,
),
child: Icon(Icons.person_off_outlined,
size: 48, color: cs.onSurfaceVariant),
),
const SizedBox(height: 16),
Text('لا يوجد ركاب',
style: TextStyle(
color: cs.onSurfaceVariant, fontSize: 14)),
],
),
);
@@ -187,132 +308,171 @@ class Passengrs extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 16),
physics: const BouncingScrollPhysics(),
itemCount: passengers.length,
separatorBuilder: (context, index) => const SizedBox(height: 12),
separatorBuilder: (context, index) => const SizedBox(height: 10),
itemBuilder: (context, index) {
final user = passengers[index];
return _buildPassengerItem(user, isSuperAdmin);
return _buildPassengerItem(user, isSuperAdmin, cs);
},
);
}
// --- عنصر الراكب الواحد (Card) ---
Widget _buildPassengerItem(dynamic user, bool isSuperAdmin) {
Widget _buildPassengerItem(
dynamic user, bool isSuperAdmin, ColorScheme cs) {
String firstName = user['first_name'] ?? '';
String lastName = user['last_name'] ?? '';
String fullName = '$firstName $lastName'.trim();
if (fullName.isEmpty) fullName = 'Unknown User';
if (fullName.isEmpty) fullName = 'مستخدم';
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
border: Border.all(color: Colors.grey.withOpacity(0.1)),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.05),
blurRadius: 5,
offset: const Offset(0, 2),
),
],
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: cs.outline),
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(15),
borderRadius: BorderRadius.circular(16),
onTap: () {
// الانتقال للتفاصيل مع تمرير صلاحية الأدمن
Get.to(
() => const PassengerDetailsPage(),
arguments: {'data': user, 'isSuperAdmin': isSuperAdmin},
);
},
child: Padding(
padding: const EdgeInsets.all(12.0),
padding: const EdgeInsets.all(14),
child: Row(
children: [
// Avatar
CircleAvatar(
radius: 25,
backgroundColor: AppColor.primaryColor.withOpacity(0.1),
child: Text(
fullName.isNotEmpty ? fullName[0].toUpperCase() : 'U',
style: TextStyle(
color: AppColor.primaryColor,
fontWeight: FontWeight.bold,
fontSize: 18),
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
cs.primary.withValues(alpha: 0.20),
cs.primary.withValues(alpha: 0.08),
],
),
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: cs.primary.withValues(alpha: 0.2)),
),
child: Center(
child: Text(
fullName.isNotEmpty
? fullName[0].toUpperCase()
: 'U',
style: TextStyle(
color: cs.primary,
fontWeight: FontWeight.w800,
fontSize: 18,
),
),
),
),
const SizedBox(width: 15),
// Info
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
fullName,
style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 16),
style: TextStyle(
color: cs.onSurface,
fontWeight: FontWeight.w600,
fontSize: 15,
),
),
const SizedBox(height: 4),
// Stats Row
const SizedBox(height: 6),
Row(
children: [
Icon(Icons.star_rounded,
size: 16, color: Colors.amber[700]),
Text(
" ${user['ratingPassenger'] ?? '0.0'} ",
style: const TextStyle(
fontSize: 12, fontWeight: FontWeight.bold),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFFF59E0B)
.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(6),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.star_rounded,
size: 12, color: Color(0xFFF59E0B)),
const SizedBox(width: 3),
Text(
user['ratingPassenger']?.toString() ??
'0.0',
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
color: Color(0xFFF59E0B),
),
),
],
),
),
const SizedBox(width: 8),
Icon(Icons.directions_car,
size: 14, color: Colors.grey[400]),
Text(
" ${user['countPassengerRide'] ?? '0'} Trips",
style: TextStyle(
fontSize: 12, color: Colors.grey[600]),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: cs.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(6),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.directions_car_rounded,
size: 12, color: cs.primary),
const SizedBox(width: 3),
Text(
'${user['countPassengerRide'] ?? '0'} رحلة',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: cs.primary,
),
),
],
),
),
],
),
const SizedBox(height: 4),
// Phone Number (Masked logic)
const SizedBox(height: 6),
Row(
children: [
Icon(Icons.phone_iphone,
size: 12, color: Colors.grey[400]),
Icon(Icons.phone_iphone_rounded,
size: 12, color: cs.onSurfaceVariant),
const SizedBox(width: 4),
Text(
_formatPhoneNumber(
user['phone'].toString(), isSuperAdmin),
style: TextStyle(
fontSize: 12,
color: Colors.grey[500],
fontFamily: 'monospace'),
fontSize: 11,
color: cs.onSurfaceVariant,
fontFamily: 'monospace',
),
),
],
),
// Email (Show only if Super Admin)
if (isSuperAdmin && user['email'] != null) ...[
const SizedBox(height: 2),
const SizedBox(height: 3),
Text(
user['email'],
style:
TextStyle(fontSize: 10, color: Colors.grey[400]),
style: TextStyle(
fontSize: 10, color: cs.onSurfaceVariant),
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
]
),
],
],
),
),
// Arrow
Icon(Icons.arrow_forward_ios_rounded,
size: 16, color: Colors.grey[300]),
size: 14, color: cs.onSurfaceVariant),
],
),
),
@@ -321,63 +481,89 @@ class Passengrs extends StatelessWidget {
);
}
// --- دالة تنسيق الرقم (إظهار آخر 4 أرقام لغير الأدمن) ---
String _formatPhoneNumber(String phone, bool isSuperAdmin) {
if (isSuperAdmin) return phone; // إظهار الرقم كاملاً للسوبر أدمن
// لغير الأدمن
if (isSuperAdmin) return phone;
if (phone.length <= 4) return phone;
String lastFour = phone.substring(phone.length - 4);
String masked = '*' * (phone.length - 4);
return '$masked$lastFour'; // النتيجة: *******5678
return '$masked$lastFour';
}
// --- دالة إضافة الجوائز ---
void _showAddPrizeDialog(PassengerAdminController controller) {
// التحقق من يوم السبت
void _showAddPrizeDialog(
PassengerAdminController controller, ColorScheme cs) {
if (DateTime.now().weekday == DateTime.saturday) {
Get.defaultDialog(
title: 'Add Prize'.tr,
titleStyle: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
title: 'إضافة جائزة',
titleStyle: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
color: cs.onSurface),
backgroundColor: cs.surface,
contentPadding: const EdgeInsets.all(20),
content: Form(
key: controller.formPrizeKey,
child: Column(
children: [
Text(
'Add Points to Gold Passengers wallet'.tr,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
content: Column(
children: [
Text(
'إضافة نقاط لمحفظة ركاب الذهب',
textAlign: TextAlign.center,
style:
TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
),
const SizedBox(height: 20),
TextFormField(
controller: controller.passengerPrizeController,
keyboardType: TextInputType.number,
style: TextStyle(color: cs.onSurface),
cursorColor: cs.primary,
decoration: InputDecoration(
labelText: 'مبلغ الجائزة',
hintText: '1000...',
filled: true,
fillColor: cs.surfaceContainerHighest,
labelStyle: TextStyle(color: cs.onSurfaceVariant),
hintStyle: TextStyle(color: cs.onSurfaceVariant),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: cs.outline),
borderRadius: BorderRadius.circular(14),
),
focusedBorder: OutlineInputBorder(
borderSide:
BorderSide(color: cs.primary, width: 1.5),
borderRadius: BorderRadius.circular(14),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 14),
),
const SizedBox(height: 20),
MyTextForm(
controller: controller.passengerPrizeController,
label: 'Prize Amount'.tr,
hint: '1000...',
type: TextInputType.number,
),
],
),
),
],
),
confirm: SizedBox(
width: 120,
child: MyElevatedButton(
title: 'Add',
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: cs.primary,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
elevation: 0,
),
onPressed: () async {
if (controller.formPrizeKey.currentState!.validate()) {
controller.addPassengerPrizeToWalletSecure();
Get.back();
}
},
child: const Text('إضافة',
style: TextStyle(fontWeight: FontWeight.bold)),
),
),
cancel: TextButton(
onPressed: () => Get.back(),
child:
Text('Cancel'.tr, style: const TextStyle(color: Colors.grey))),
onPressed: () => Get.back(),
child: Text('إلغاء',
style: TextStyle(color: cs.onSurfaceVariant)),
),
);
} else {
mySnackbarWarning('Prizes can only be added on Saturdays.'.tr);
mySnackbarWarning('يمكن إضافة الجوائز يوم السبت فقط');
}
}
}
@@ -1,10 +1,8 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:get/get.dart';
import '../../../constant/colors.dart';
import '../../../constant/style.dart';
import '../../../controller/admin/kazan_controller.dart';
import '../../../views/widgets/snackbar.dart';
import '../../widgets/my_scafold.dart';
import '../../widgets/elevated_btn.dart';
class KazanEditorPage extends StatelessWidget {
@@ -14,63 +12,72 @@ class KazanEditorPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MyScafolld(
title: 'تعديل أسعار كازان'.tr,
isleading: true,
body: [
Column(
children: [
_buildCountryDropdown(),
Expanded(
child: Obx(() => controller.isLoading.value && controller.kazanData.isEmpty
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionHeader('⚙️ الإعدادات العامة'),
_buildGeneralSettings(),
const SizedBox(height: 24),
_buildSectionHeader('🚗 أسعار الكيلومتر لكل نوع سيارة'),
_buildKmPricesGrid(),
const SizedBox(height: 24),
_buildSectionHeader('⏱️ أسعار الدقيقة (حسب وقت اليوم)'),
_buildMinutePrices(),
const SizedBox(height: 32),
MyElevatedButton(
title: '💾 حفظ جميع التعديلات',
icon: Icons.save_rounded,
onPressed: () => _handleSave(),
),
const SizedBox(height: 100),
],
),
)),
),
],
final cs = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: cs.surface,
appBar: AppBar(
backgroundColor: cs.surface,
elevation: 0,
centerTitle: true,
title: Text('تعديل أسعار كازان'.tr, style: GoogleFonts.inter(fontWeight: FontWeight.w600, color: cs.onSurface)),
leading: GestureDetector(
onTap: () => Get.back(),
child: Icon(Icons.arrow_back_ios_new_rounded, color: cs.onSurface),
),
],
),
body: Column(
children: [
_buildCountryDropdown(cs),
Expanded(
child: Obx(() => controller.isLoading.value && controller.kazanData.isEmpty
? Center(child: CircularProgressIndicator(color: cs.primary))
: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionHeader('⚙️ الإعدادات العامة', cs),
_buildGeneralSettings(cs),
const SizedBox(height: 24),
_buildSectionHeader('🚗 أسعار الكيلومتر لكل نوع سيارة', cs),
_buildKmPricesGrid(cs),
const SizedBox(height: 24),
_buildSectionHeader('⏱️ أسعار الدقيقة (حسب وقت اليوم)', cs),
_buildMinutePrices(cs),
const SizedBox(height: 32),
MyElevatedButton(
title: '💾 حفظ جميع التعديلات',
icon: Icons.save_rounded,
onPressed: () => _handleSave(),
),
const SizedBox(height: 100),
],
),
)),
),
],
),
);
}
Widget _buildCountryDropdown() {
Widget _buildCountryDropdown(ColorScheme cs) {
return Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
child: Obx(() => Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: AppColor.surface,
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: AppColor.divider),
border: Border.all(color: cs.outline),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: controller.selectedCountry.value,
isExpanded: true,
icon: const Icon(Icons.keyboard_arrow_down_rounded),
style: AppStyle.title.copyWith(fontSize: 16),
style: GoogleFonts.inter(fontWeight: FontWeight.w600, fontSize: 16, color: cs.onSurface),
items: controller.countries.map((c) {
return DropdownMenuItem<String>(
value: c['name'],
@@ -92,13 +99,14 @@ class KazanEditorPage extends StatelessWidget {
);
}
// ====================================================================
// 1. الإعدادات العامة
// ====================================================================
Widget _buildGeneralSettings() {
Widget _buildGeneralSettings(ColorScheme cs) {
return Container(
padding: const EdgeInsets.all(20),
decoration: AppStyle.cardDecoration,
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: cs.outline),
),
child: Column(
children: [
_buildSliderItem(
@@ -106,31 +114,30 @@ class KazanEditorPage extends StatelessWidget {
'kazanPercent',
'النسبة المئوية التي تقتطعها المنصة من كل رحلة',
Icons.percent_rounded,
cs,
),
const Divider(height: 32, color: AppColor.divider),
Divider(height: 32, color: cs.outline),
_buildPriceInputRow(
'سعر الوقود المرجعي',
'fuelPrice',
'السعر المستخدم في حسابات تعويض الوقود',
Icons.local_gas_station_rounded,
cs,
),
const Divider(height: 32, color: AppColor.divider),
Divider(height: 32, color: cs.outline),
_buildPriceInputRow(
'رمز العملة',
'currency',
'مثال: SYP, EGP, JOD, IQD',
Icons.currency_exchange_rounded,
cs,
),
],
),
);
}
// ====================================================================
// 2. أسعار الكيلومتر - كل نوع سيارة له حقل خاص به
// ====================================================================
Widget _buildKmPricesGrid() {
// 🆕 9 أنواع سيارات - كل واحد له عمود سعر مستقل
Widget _buildKmPricesGrid(ColorScheme cs) {
final Map<String, Map<String, dynamic>> priceFields = {
'speedPrice': {
'label': 'Speed ⚡',
@@ -180,13 +187,17 @@ class KazanEditorPage extends StatelessWidget {
};
return Container(
decoration: AppStyle.cardDecoration,
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: cs.outline),
),
padding: const EdgeInsets.all(12),
child: GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3, // 3 أعمدة بدلاً من 2
crossAxisCount: 3,
childAspectRatio: 1.8,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
@@ -196,14 +207,14 @@ class KazanEditorPage extends StatelessWidget {
String key = priceFields.keys.elementAt(index);
var field = priceFields[key]!;
return _buildKmPriceCard(
key, field['label'], field['icon'], field['color']);
key, field['label'], field['icon'], field['color'], cs);
},
),
);
}
Widget _buildKmPriceCard(
String key, String label, IconData icon, Color color) {
String key, String label, IconData icon, Color color, ColorScheme cs) {
final TextEditingController textController =
TextEditingController(text: _getValue(key));
@@ -225,8 +236,7 @@ class KazanEditorPage extends StatelessWidget {
Expanded(
child: Text(
label,
style: AppStyle.caption
.copyWith(fontSize: 10, fontWeight: FontWeight.bold),
style: GoogleFonts.inter(fontSize: 10, fontWeight: FontWeight.bold, color: cs.onSurfaceVariant),
overflow: TextOverflow.ellipsis,
),
),
@@ -240,7 +250,7 @@ class KazanEditorPage extends StatelessWidget {
controller: textController,
keyboardType: TextInputType.number,
textAlign: TextAlign.center,
style: AppStyle.number.copyWith(fontSize: 13, color: color),
style: GoogleFonts.jetBrainsMono(fontSize: 13, fontWeight: FontWeight.bold, color: color),
decoration: const InputDecoration(
border: InputBorder.none,
isDense: true,
@@ -256,10 +266,7 @@ class KazanEditorPage extends StatelessWidget {
);
}
// ====================================================================
// 3. أسعار الدقيقة (حسب وقت اليوم)
// ====================================================================
Widget _buildMinutePrices() {
Widget _buildMinutePrices(ColorScheme cs) {
final Map<String, Map<String, dynamic>> minuteFields = {
'normalMinPrice': {
'label': 'Normal (سعر الدقيقة العادي)',
@@ -282,7 +289,11 @@ class KazanEditorPage extends StatelessWidget {
};
return Container(
decoration: AppStyle.cardDecoration,
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: cs.outline),
),
padding: const EdgeInsets.all(16),
child: Column(
children: minuteFields.entries.map((entry) {
@@ -293,6 +304,7 @@ class KazanEditorPage extends StatelessWidget {
entry.key,
entry.value['desc'],
entry.value['icon'],
cs,
),
);
}).toList(),
@@ -300,15 +312,12 @@ class KazanEditorPage extends StatelessWidget {
);
}
// ====================================================================
// دوال مساعدة للـ UI
// ====================================================================
Widget _buildSectionHeader(String title) {
Widget _buildSectionHeader(String title, ColorScheme cs) {
return Padding(
padding: const EdgeInsets.only(bottom: 12, left: 4),
child: Text(
title,
style: AppStyle.title.copyWith(color: AppColor.accent),
style: GoogleFonts.inter(fontWeight: FontWeight.w600, color: cs.primary),
),
);
}
@@ -322,21 +331,21 @@ class KazanEditorPage extends StatelessWidget {
}
Widget _buildPriceInputRow(
String title, String key, String desc, IconData icon) {
String title, String key, String desc, IconData icon, ColorScheme cs) {
final TextEditingController textController =
TextEditingController(text: _getValue(key));
return Row(
children: [
Icon(icon, size: 20, color: AppColor.accent),
Icon(icon, size: 20, color: cs.primary),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: AppStyle.body.copyWith(fontWeight: FontWeight.bold)),
Text(desc, style: AppStyle.caption.copyWith(fontSize: 10)),
style: GoogleFonts.inter(fontSize: 14, color: cs.onSurface, fontWeight: FontWeight.bold)),
Text(desc, style: GoogleFonts.inter(fontSize: 10, color: cs.onSurfaceVariant)),
],
),
),
@@ -344,16 +353,15 @@ class KazanEditorPage extends StatelessWidget {
width: 100,
height: 40,
decoration: BoxDecoration(
color: AppColor.surfaceElevated,
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColor.divider),
border: Border.all(color: cs.outline),
),
child: TextField(
controller: textController,
keyboardType: TextInputType.number,
textAlign: TextAlign.center,
style:
AppStyle.number.copyWith(fontSize: 16, color: AppColor.accent),
style: GoogleFonts.jetBrainsMono(fontSize: 16, fontWeight: FontWeight.bold, color: cs.primary),
decoration: const InputDecoration(
border: InputBorder.none,
isDense: true,
@@ -367,7 +375,7 @@ class KazanEditorPage extends StatelessWidget {
}
Widget _buildSliderItem(
String title, String key, String desc, IconData icon) {
String title, String key, String desc, IconData icon, ColorScheme cs) {
double value = double.tryParse(_getValue(key)) ?? 0;
return Column(
@@ -378,25 +386,25 @@ class KazanEditorPage extends StatelessWidget {
children: [
Row(
children: [
Icon(icon, size: 18, color: AppColor.accent),
Icon(icon, size: 18, color: cs.primary),
const SizedBox(width: 8),
Text(title, style: AppStyle.title),
Text(title, style: GoogleFonts.inter(fontWeight: FontWeight.w600, color: cs.onSurface)),
],
),
Text(
'${value.toInt()}%',
style: AppStyle.number.copyWith(fontSize: 18),
style: GoogleFonts.jetBrainsMono(fontSize: 18, fontWeight: FontWeight.bold, color: cs.onSurface),
),
],
),
const SizedBox(height: 4),
Text(desc, style: AppStyle.caption),
Text(desc, style: GoogleFonts.inter(color: cs.onSurfaceVariant, fontSize: 12)),
Slider(
value: value.clamp(0, 100),
min: 0,
max: 100,
activeColor: AppColor.accent,
inactiveColor: AppColor.divider,
activeColor: cs.primary,
inactiveColor: cs.outline,
onChanged: (val) {
_setValue(key, val.toInt().toString());
},
@@ -405,14 +413,10 @@ class KazanEditorPage extends StatelessWidget {
);
}
// ====================================================================
// حفظ البيانات
// ====================================================================
void _handleSave() async {
final data = Map<String, dynamic>.from(controller.kazanData);
data['adminId'] = 'admin1';
// التأكد من وجود country
if (!data.containsKey('country') ||
data['country'] == null ||
data['country'].toString().isEmpty) {
@@ -1,9 +1,7 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:get/get.dart';
import '../../../constant/colors.dart';
import '../../../constant/style.dart';
import '../../../controller/admin/promo_controller.dart';
import '../../widgets/my_scafold.dart';
import '../../widgets/elevated_btn.dart';
import '../../widgets/my_textField.dart';
import '../../widgets/mydialoug.dart';
@@ -15,89 +13,110 @@ class PromoManagementPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MyScafolld(
title: 'إدارة أكواد الخصم'.tr,
isleading: true,
action: IconButton(
icon: const Icon(Icons.add_circle_outline_rounded, color: AppColor.accent),
onPressed: () => _showPromoSheet(context),
final cs = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: cs.surface,
appBar: AppBar(
backgroundColor: cs.surface,
elevation: 0,
centerTitle: true,
title: Text('إدارة أكواد الخصم'.tr, style: GoogleFonts.inter(fontWeight: FontWeight.w600, color: cs.onSurface)),
leading: GestureDetector(
onTap: () => Get.back(),
child: Icon(Icons.arrow_back_ios_new_rounded, color: cs.onSurface),
),
actions: [
IconButton(
icon: Icon(Icons.add_circle_outline_rounded, color: cs.primary),
onPressed: () => _showPromoSheet(context),
),
],
),
body: [
Obx(() => controller.isLoading.value && controller.promoList.isEmpty
? const Center(child: CircularProgressIndicator())
: controller.promoList.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.confirmation_number_outlined, size: 64, color: AppColor.textMuted),
const SizedBox(height: 16),
Text('لا يوجد أكواد خصم حالياً', style: AppStyle.subtitle),
],
),
)
: RefreshIndicator(
onRefresh: () => controller.getPromos(),
child: ListView.builder(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 80),
itemCount: controller.promoList.length,
itemBuilder: (context, index) {
final promo = controller.promoList[index];
return _buildPromoCard(context, promo);
},
),
)),
],
body: Obx(() => controller.isLoading.value && controller.promoList.isEmpty
? Center(child: CircularProgressIndicator(color: cs.primary))
: controller.promoList.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(shape: BoxShape.circle, color: cs.onSurfaceVariant.withValues(alpha: 0.1)),
child: Icon(Icons.confirmation_number_outlined, size: 48, color: cs.onSurfaceVariant),
),
const SizedBox(height: 16),
Text('لا يوجد أكواد خصم حالياً', style: GoogleFonts.inter(color: cs.onSurfaceVariant, fontSize: 14)),
],
),
)
: RefreshIndicator(
onRefresh: () => controller.getPromos(),
child: ListView.builder(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 80),
itemCount: controller.promoList.length,
itemBuilder: (context, index) {
final promo = controller.promoList[index];
return _buildPromoCard(context, promo);
},
),
)),
);
}
Widget _buildPromoCard(BuildContext context, dynamic promo) {
final cs = Theme.of(context).colorScheme;
return Container(
margin: const EdgeInsets.only(bottom: 12),
decoration: AppStyle.cardDecoration,
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: cs.outline),
),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
leading: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: AppColor.accentSoft,
color: cs.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(Icons.local_offer_rounded, color: AppColor.accent),
child: Icon(Icons.local_offer_rounded, color: cs.primary),
),
title: Text(
promo['promo_code']?.toString() ?? 'N/A',
style: AppStyle.title,
style: GoogleFonts.inter(fontWeight: FontWeight.w600, color: cs.onSurface),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
Text(promo['description']?.toString() ?? '', style: AppStyle.caption),
Text(promo['description']?.toString() ?? '', style: GoogleFonts.inter(color: cs.onSurfaceVariant, fontSize: 12)),
const SizedBox(height: 4),
Row(
children: [
Icon(Icons.money_rounded, size: 14, color: AppColor.success),
Icon(Icons.money_rounded, size: 14, color: const Color(0xFF10B981)),
const SizedBox(width: 4),
Text('% ${promo['amount']}', style: AppStyle.number.copyWith(color: AppColor.success)),
Text('% ${promo['amount']}', style: GoogleFonts.jetBrainsMono(fontWeight: FontWeight.bold, color: const Color(0xFF10B981))),
const SizedBox(width: 12),
Icon(Icons.person_rounded, size: 14, color: AppColor.info),
Icon(Icons.person_rounded, size: 14, color: cs.tertiary),
const SizedBox(width: 4),
Text(promo['passengerID'] == 'none' ? 'عام' : 'مخصص', style: AppStyle.caption),
Text(promo['passengerID'] == 'none' ? 'عام' : 'مخصص', style: GoogleFonts.inter(color: cs.onSurfaceVariant, fontSize: 12)),
],
),
],
),
trailing: PopupMenuButton(
icon: const Icon(Icons.more_vert_rounded, color: AppColor.textSecondary),
color: AppColor.surfaceElevated,
icon: Icon(Icons.more_vert_rounded, color: cs.onSurfaceVariant),
color: cs.surfaceContainerHighest,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
itemBuilder: (context) => [
PopupMenuItem(
value: 'edit',
child: Row(
children: [
const Icon(Icons.edit_rounded, size: 18, color: AppColor.info),
Icon(Icons.edit_rounded, size: 18, color: cs.tertiary),
const SizedBox(width: 8),
Text('تعديل'.tr),
],
@@ -107,9 +126,9 @@ class PromoManagementPage extends StatelessWidget {
value: 'delete',
child: Row(
children: [
const Icon(Icons.delete_outline_rounded, size: 18, color: AppColor.danger),
Icon(Icons.delete_outline_rounded, size: 18, color: cs.error),
const SizedBox(width: 8),
Text('حذف'.tr, style: const TextStyle(color: AppColor.danger)),
Text('حذف'.tr, style: TextStyle(color: cs.error)),
],
),
),
@@ -131,6 +150,7 @@ class PromoManagementPage extends StatelessWidget {
}
void _showPromoSheet(BuildContext context, {dynamic promo}) {
final cs = Theme.of(context).colorScheme;
final TextEditingController codeController = TextEditingController(text: promo?['promo_code']);
final TextEditingController amountController = TextEditingController(text: promo?['amount']?.toString());
final TextEditingController descController = TextEditingController(text: promo?['description']);
@@ -141,9 +161,9 @@ class PromoManagementPage extends StatelessWidget {
Get.bottomSheet(
Container(
padding: const EdgeInsets.all(24),
decoration: const BoxDecoration(
color: AppColor.surfaceElevated,
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
),
child: SingleChildScrollView(
child: Column(
@@ -152,10 +172,10 @@ class PromoManagementPage extends StatelessWidget {
Container(
width: 40,
height: 4,
decoration: BoxDecoration(color: AppColor.divider, borderRadius: BorderRadius.circular(2)),
decoration: BoxDecoration(color: cs.outline, borderRadius: BorderRadius.circular(2)),
),
const SizedBox(height: 24),
Text(promo == null ? 'إضافة كود خصم جديد' : 'تعديل كود الخصم', style: AppStyle.headTitle),
Text(promo == null ? 'إضافة كود خصم جديد' : 'تعديل كود الخصم', style: GoogleFonts.cairo(fontWeight: FontWeight.bold, color: cs.onSurface, fontSize: 18)),
const SizedBox(height: 24),
MyTextForm(
controller: codeController,
@@ -7,143 +7,324 @@ class BlacklistPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
Get.put(QualityController()).fetchBlacklist();
return DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: const Text('إدارة القائمة السوداء (Blacklist)',
style: TextStyle(fontWeight: FontWeight.bold)),
backgroundColor: Colors.red[800],
bottom: const TabBar(
indicatorColor: Colors.white,
tabs: [
Tab(icon: Icon(Icons.drive_eta), text: 'السائقين المحظورين'),
Tab(icon: Icon(Icons.person), text: 'الركاب المحظورين'),
],
),
),
body: GetBuilder<QualityController>(
builder: (controller) {
if (controller.isLoading) {
return const Center(child: CircularProgressIndicator());
}
return Scaffold(
backgroundColor: cs.surface,
body: Column(
children: [
_buildAppBar(context, cs),
_buildTabBar(context, cs),
Expanded(
child: GetBuilder<QualityController>(
builder: (controller) {
if (controller.isLoading) {
return _buildLoadingState(cs);
}
return TabBarView(
children: [
_buildDriverList(controller),
_buildPassengerList(controller),
],
);
},
return TabBarView(
controller: DefaultTabController.of(context),
children: [
_buildDriverList(controller, cs),
_buildPassengerList(controller, cs),
],
);
},
),
),
],
),
);
}
Widget _buildAppBar(BuildContext context, ColorScheme cs) {
return Container(
padding: const EdgeInsets.fromLTRB(16, 50, 16, 16),
decoration: BoxDecoration(
color: cs.surface,
border: Border(bottom: BorderSide(color: cs.outline)),
),
child: Row(
children: [
GestureDetector(
onTap: () => Get.back(),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: cs.outline),
),
child: Icon(Icons.arrow_back_ios_new_rounded,
color: cs.onSurfaceVariant, size: 16),
),
),
const SizedBox(width: 12),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: cs.error.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: cs.error.withValues(alpha: 0.25)),
),
child: Icon(Icons.block_rounded, color: cs.error, size: 18),
),
const SizedBox(width: 10),
Text(
'القائمة السوداء',
style: TextStyle(
color: cs.onSurface,
fontSize: 18,
fontWeight: FontWeight.w700,
),
),
],
),
);
}
Widget _buildTabBar(BuildContext context, ColorScheme cs) {
return Container(
margin: const EdgeInsets.fromLTRB(16, 12, 16, 0),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: cs.outline),
),
child: TabBar(
controller: DefaultTabController.of(context),
indicator: BoxDecoration(
color: cs.primary.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: cs.primary.withValues(alpha: 0.3)),
),
indicatorSize: TabBarIndicatorSize.tab,
dividerColor: Colors.transparent,
labelColor: cs.primary,
unselectedLabelColor: cs.onSurfaceVariant,
labelStyle: const TextStyle(fontWeight: FontWeight.w700, fontSize: 13),
unselectedLabelStyle: const TextStyle(fontWeight: FontWeight.w500, fontSize: 13),
padding: const EdgeInsets.all(4),
labelPadding: EdgeInsets.zero,
tabs: const [
Tab(text: 'السائقين', icon: Icon(Icons.drive_eta_rounded, size: 16)),
Tab(text: 'الركاب', icon: Icon(Icons.person_rounded, size: 16)),
],
),
);
}
Widget _buildLoadingState(ColorScheme cs) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 40,
height: 40,
child: CircularProgressIndicator(
color: cs.primary,
strokeWidth: 2,
backgroundColor: cs.primary.withValues(alpha: 0.1),
),
),
const SizedBox(height: 16),
Text('جاري التحميل...',
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13)),
],
),
);
}
Widget _buildDriverList(QualityController controller, ColorScheme cs) {
if (controller.driversBlacklist.isEmpty) {
return _buildEmptyState('لا يوجد سائقين محظورين', cs);
}
return ListView.builder(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 80),
physics: const BouncingScrollPhysics(),
itemCount: controller.driversBlacklist.length,
itemBuilder: (context, index) {
var driver = controller.driversBlacklist[index];
return _buildBlacklistItem(
phone: driver['phone'],
reason: driver['reason'] ?? 'غير محدد',
date: driver['created_at'],
type: 'سائق',
icon: Icons.drive_eta_rounded,
onUnblock: () => controller.unblockDriver(driver['phone'].toString()),
cs: cs,
);
},
);
}
Widget _buildPassengerList(QualityController controller, ColorScheme cs) {
if (controller.passengersBlacklist.isEmpty) {
return _buildEmptyState('لا يوجد ركاب محظورين', cs);
}
return ListView.builder(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 80),
physics: const BouncingScrollPhysics(),
itemCount: controller.passengersBlacklist.length,
itemBuilder: (context, index) {
var passenger = controller.passengersBlacklist[index];
return _buildBlacklistItem(
phone: passenger['phone'] ?? passenger['phone_normalized'],
reason: passenger['reason'] ?? 'غير محدد',
date: passenger['created_at'],
type: 'راكب',
icon: Icons.person_rounded,
onUnblock: () => controller.unblockPassenger(
passenger['phone_normalized'].toString()),
cs: cs,
);
},
);
}
Widget _buildEmptyState(String message, ColorScheme cs) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
shape: BoxShape.circle,
),
child: Icon(Icons.block_rounded, size: 48, color: cs.onSurfaceVariant),
),
const SizedBox(height: 16),
Text(message,
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 14)),
],
),
);
}
Widget _buildBlacklistItem({
required String phone,
required String reason,
required String date,
required String type,
required IconData icon,
required VoidCallback onUnblock,
required ColorScheme cs,
}) {
return Container(
margin: const EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: cs.error.withValues(alpha: 0.15)),
),
child: Padding(
padding: const EdgeInsets.all(14),
child: Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: cs.error.withValues(alpha: 0.12),
shape: BoxShape.circle,
border: Border.all(color: cs.error.withValues(alpha: 0.2)),
),
child: Icon(icon, color: cs.error, size: 22),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
phone,
style: TextStyle(
color: cs.onSurface,
fontWeight: FontWeight.w700,
fontSize: 15,
fontFamily: 'monospace',
),
),
const SizedBox(height: 4),
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: cs.error.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(6),
),
child: Text(
type,
style: TextStyle(
color: cs.error,
fontSize: 10,
fontWeight: FontWeight.w700,
),
),
),
const SizedBox(width: 6),
Expanded(
child: Text(
'السبب: $reason',
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 11,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 3),
Text(
date,
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 10,
),
),
],
),
),
const SizedBox(width: 8),
GestureDetector(
onTap: () => _showUnblockDialog(Get.context!, type, phone, onUnblock, cs),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: const Color(0xFF10B981).withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFF10B981).withValues(alpha: 0.2)),
),
child: Icon(Icons.settings_backup_restore_rounded,
color: const Color(0xFF10B981), size: 18),
),
),
],
),
),
);
}
Widget _buildDriverList(QualityController controller) {
if (controller.driversBlacklist.isEmpty) {
return const Center(child: Text('لا يوجد سائقين محظورين حالياً'));
}
return ListView.builder(
itemCount: controller.driversBlacklist.length,
padding: const EdgeInsets.all(12),
itemBuilder: (context, index) {
var driver = controller.driversBlacklist[index];
return Card(
elevation: 3,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
child: ListTile(
leading: const CircleAvatar(
backgroundColor: Colors.redAccent,
child: Icon(Icons.block, color: Colors.white),
),
title: Text('هاتف: ${driver['phone']}',
style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('السبب: ${driver['reason'] ?? "غير محدد"}'),
Text('تاريخ الحظر: ${driver['created_at']}'),
],
),
trailing: IconButton(
icon: const Icon(Icons.settings_backup_restore,
color: Colors.green),
onPressed: () {
_showUnblockDialog(
Get.context!,
'سائق',
driver['phone'],
() => controller.unblockDriver(driver['phone'].toString()),
);
},
),
),
);
},
);
}
Widget _buildPassengerList(QualityController controller) {
if (controller.passengersBlacklist.isEmpty) {
return const Center(child: Text('لا يوجد ركاب محظورين حالياً'));
}
return ListView.builder(
itemCount: controller.passengersBlacklist.length,
padding: const EdgeInsets.all(12),
itemBuilder: (context, index) {
var passenger = controller.passengersBlacklist[index];
return Card(
elevation: 3,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
child: ListTile(
leading: const CircleAvatar(
backgroundColor: Colors.orangeAccent,
child: Icon(Icons.person_off, color: Colors.white),
),
title: Text(
'هاتف: ${passenger['phone'] ?? passenger['phone_normalized']}',
style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('السبب: ${passenger['reason'] ?? "غير محدد"}'),
Text('تاريخ الحظر: ${passenger['created_at']}'),
],
),
trailing: IconButton(
icon: const Icon(Icons.settings_backup_restore,
color: Colors.green),
onPressed: () {
_showUnblockDialog(
Get.context!,
'راكب',
passenger['phone_normalized'],
() => controller.unblockPassenger(
passenger['phone_normalized'].toString()),
);
},
),
),
);
},
);
}
void _showUnblockDialog(BuildContext context, String type, String identifier,
VoidCallback onConfirm) {
void _showUnblockDialog(
BuildContext context, String type, String identifier, VoidCallback onConfirm, ColorScheme cs) {
Get.defaultDialog(
title: "تأكيد فك الحظر",
titleStyle: TextStyle(color: cs.onSurface, fontWeight: FontWeight.bold),
backgroundColor: cs.surface,
middleText:
"هل أنت متأكد من فك الحظر عن هذا ال$type ($identifier)؟\nسيتم تسجيل هذه العملية في الـ Audit Log.",
textConfirm: "نعم، فك الحظر",
textCancel: "تراجع",
confirmTextColor: Colors.white,
buttonColor: Colors.green,
buttonColor: const Color(0xFF10B981),
cancelTextColor: cs.onSurfaceVariant,
onConfirm: () {
Get.back();
onConfirm();
+418 -219
View File
@@ -1,236 +1,435 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../constant/colors.dart';
import '../../../constant/style.dart';
import '../../../controller/admin/ride_admin_controller.dart';
import '../../widgets/my_scafold.dart';
import '../../widgets/mycircular.dart';
class Rides extends StatelessWidget {
Rides({super.key});
final RideAdminController rideAdminController = Get.put(RideAdminController());
@override
Widget build(BuildContext context) {
return MyScafolld(title: 'Rides'.tr, isleading: true, body: [
GetBuilder<RideAdminController>(
builder: (rideAdminController) => rideAdminController.isLoading
? const Center(child: MyCircularProgressIndicator())
: Column(
children: [
SizedBox(
height: Get.height * .4,
child: LineChart(
duration: const Duration(milliseconds: 150),
curve: Curves.ease,
LineChartData(
lineBarsData: [
LineChartBarData(
spots: rideAdminController.chartData,
isCurved: true,
color: Colors.deepPurpleAccent, // Custom color
barWidth: 3, // Thinner line
dotData: const FlDotData(
show: true), // Show dots on each point
belowBarData: BarAreaData(
// Add gradient fill below the line
show: true,
color: AppColor.deepPurpleAccent,
),
isStrokeJoinRound: true,
shadow: const BoxShadow(
color: AppColor.yellowColor,
blurRadius: 4,
offset: Offset(2, 2),
),
),
],
showingTooltipIndicators: const [],
titlesData: FlTitlesData(
show: true,
topTitles: AxisTitles(
axisNameWidget: Text(
'Days',
style: AppStyle.title,
),
axisNameSize: 30,
sideTitles: const SideTitles(
reservedSize: 30, showTitles: true)),
bottomTitles: AxisTitles(
axisNameWidget: Text(
'Total Trips on month'.tr,
style: AppStyle.title,
),
axisNameSize: 30,
sideTitles: const SideTitles(
reservedSize: 30, showTitles: true)),
leftTitles: AxisTitles(
axisNameWidget: Text(
'Counts of Trips on month'.tr,
style: AppStyle.title,
),
axisNameSize: 30,
sideTitles: const SideTitles(
reservedSize: 30, showTitles: true)),
),
gridData: const FlGridData(
show: true,
),
borderData: FlBorderData(
show: true,
border: const Border(
bottom: BorderSide(color: AppColor.accentColor),
left: BorderSide(color: AppColor.accentColor),
),
),
),
),
),
// SizedBox(
// height: Get.height * .4,
// child: PieChart(
// PieChartData(
// sectionsSpace: 4, // Adjust spacing between sections
// centerSpaceRadius:
// 40, // Adjust radius of center space
// sections: [
// for (final rideData in rideAdminController.rideData)
// PieChartSectionData(
// value: rideData.ridesCount.toDouble(),
// title: '${rideData.day}', showTitle: true,
// titleStyle:
// AppStyle.subtitle, // Display day as title
// radius: 60, // Adjust radius of each section
// color:
// AppColor.deepPurpleAccent, // Custom color
// ),
// ],
// ),
// ),
// ),
final cs = Theme.of(context).colorScheme;
// SizedBox(
// // height: 400,
// child: SfCartesianChart(
// legend: const Legend(
// isVisible: true,
// position: LegendPosition.bottom,
// overflowMode: LegendItemOverflowMode.wrap,
// textStyle: TextStyle(
// color: Colors.white,
// fontSize: 12,
// fontWeight: FontWeight.bold,
// ),
// ),
// borderWidth: 2,
// borderColor: AppColor.blueColor,
// plotAreaBorderColor: AppColor.deepPurpleAccent,
// enableAxisAnimation: true,
// primaryXAxis: CategoryAxis(
// borderColor: AppColor.accentColor, borderWidth: 2,
// title: AxisTitle(
// text: 'Total Trips on month'.tr,
// textStyle: AppStyle.title,
// ),
// // labelRotation: 45,
// majorGridLines: const MajorGridLines(width: 0),
// ),
// primaryYAxis: const NumericAxis(isVisible: false),
// series: <LineSeries<ChartDataS, String>>[
// LineSeries<ChartDataS, String>(
// dataSource: rideAdminController.chartDatasync,
// xValueMapper: (ChartDataS data, _) => '${data.day}',
// yValueMapper: (ChartDataS data, _) =>
// data.ridesCount,
// dataLabelSettings:
// const DataLabelSettings(isVisible: true),
// ),
// ],
// ),
// ),
return Scaffold(
backgroundColor: cs.surface,
body: Column(
children: [
_buildAppBar(context, cs),
Expanded(
child: GetBuilder<RideAdminController>(
builder: (controller) {
if (controller.isLoading) {
return _buildLoadingState(cs);
}
const SizedBox(
height: 20,
),
Card(
elevation: 4,
color: AppColor.deepPurpleAccent,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'Total Trips on this Month is ${rideAdminController.jsonResponse['message'][0]['current_month_rides_count']}',
style: AppStyle.title,
),
return SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildChartSection(controller, cs),
const SizedBox(height: 16),
_buildMonthlyTotal(controller, cs),
const SizedBox(height: 16),
_buildRideDetails(controller, cs),
],
),
);
},
),
),
],
),
);
}
Widget _buildAppBar(BuildContext context, ColorScheme cs) {
return Container(
padding: const EdgeInsets.fromLTRB(16, 50, 16, 16),
decoration: BoxDecoration(
color: cs.surface,
border: Border(bottom: BorderSide(color: cs.outline)),
),
child: Row(
children: [
GestureDetector(
onTap: () => Get.back(),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: cs.outline),
),
child: Icon(Icons.arrow_back_ios_new_rounded,
color: cs.onSurfaceVariant, size: 16),
),
),
const SizedBox(width: 12),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: cs.primary.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: cs.primary.withValues(alpha: 0.25)),
),
child: Icon(Icons.map_rounded, color: cs.primary, size: 18),
),
const SizedBox(width: 10),
Text(
'شاشة الرحلات',
style: TextStyle(
color: cs.onSurface,
fontSize: 18,
fontWeight: FontWeight.w700,
),
),
],
),
);
}
Widget _buildLoadingState(ColorScheme cs) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 40,
height: 40,
child: CircularProgressIndicator(
color: cs.primary,
strokeWidth: 2,
backgroundColor: cs.primary.withValues(alpha: 0.1),
),
),
const SizedBox(height: 16),
Text('جاري تحميل الرحلات...',
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13)),
],
),
);
}
Widget _buildChartSection(RideAdminController controller, ColorScheme cs) {
return Container(
height: 280,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: cs.primary.withValues(alpha: 0.15)),
boxShadow: [
BoxShadow(
color: cs.primary.withValues(alpha: 0.06),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: cs.primary.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: cs.primary.withValues(alpha: 0.25)),
),
child: Icon(Icons.trending_up_rounded,
color: cs.primary, size: 16),
),
const SizedBox(width: 10),
Text(
'رحلات الشهر',
style: TextStyle(
color: cs.onSurface,
fontSize: 14,
fontWeight: FontWeight.w700,
),
),
],
),
const SizedBox(height: 16),
Expanded(
child: Directionality(
textDirection: TextDirection.ltr,
child: LineChart(
LineChartData(
lineBarsData: [
LineChartBarData(
spots: controller.chartData,
isCurved: true,
color: cs.primary,
barWidth: 3,
dotData: const FlDotData(show: true),
belowBarData: BarAreaData(
show: true,
color: cs.primary.withValues(alpha: 0.08),
),
isStrokeJoinRound: true,
),
const SizedBox(
height: 20,
),
Card(
elevation: 4,
color: AppColor.yellowColor,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Text(
'Driver Average Duration: ${rideAdminController.ridesDetails[0]['driver_avg_duration']}',
style: AppStyle.subtitle,
),
Text(
'Number of Drivers: ${rideAdminController.ridesDetails[0]['num_Driver']}',
style: AppStyle.subtitle,
),
Text(
'Total Rides: ${rideAdminController.ridesDetails[0]['total_rides']}',
style: AppStyle.subtitle,
),
Text(
'Ongoing Rides: ${rideAdminController.ridesDetails[0]['ongoing_rides']}',
style: AppStyle.subtitle,
),
Text(
'Completed Rides: ${rideAdminController.ridesDetails[0]['completed_rides']}',
style: AppStyle.subtitle,
),
Text(
'Cancelled Rides: ${rideAdminController.ridesDetails[0]['cancelled_rides']}',
style: AppStyle.subtitle,
),
Text(
'Longest Duration: ${rideAdminController.ridesDetails[0]['longest_duration']}',
style: AppStyle.subtitle,
),
Text(
'Total Distance: ${rideAdminController.ridesDetails[0]['total_distance']} km',
style: AppStyle.subtitle,
),
Text(
'Average Distance: ${rideAdminController.ridesDetails[0]['average_distance']} km',
style: AppStyle.subtitle,
),
Text(
'Longest Distance: ${rideAdminController.ridesDetails[0]['longest_distance']} km',
style: AppStyle.subtitle,
),
Text(
'Total Driver Earnings: \$${rideAdminController.ridesDetails[0]['total_driver_earnings']}',
style: AppStyle.subtitle,
),
Text(
'Total Company Earnings: \$${rideAdminController.ridesDetails[0]['total_company_earnings']}',
style: AppStyle.subtitle,
),
Text(
'Company Percentage: ${rideAdminController.ridesDetails[0]['companyPercent']} %',
style: AppStyle.subtitle,
),
],
),
),
)
],
))
]);
titlesData: FlTitlesData(
show: true,
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false)),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false)),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 24,
getTitlesWidget: (v, _) => Text(
'${v.toInt()}',
style: TextStyle(
color: cs.onSurfaceVariant, fontSize: 9),
),
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 36,
getTitlesWidget: (v, _) => Text(
v.toInt().toString(),
style: TextStyle(
color: cs.onSurfaceVariant, fontSize: 9),
),
),
),
),
gridData: FlGridData(
show: true,
drawVerticalLine: false,
getDrawingHorizontalLine: (_) => FlLine(
color: cs.outline.withValues(alpha: 0.5),
strokeWidth: 1),
),
borderData: FlBorderData(show: false),
),
),
),
),
],
),
);
}
Widget _buildMonthlyTotal(RideAdminController controller, ColorScheme cs) {
final count = controller.jsonResponse['message']?[0]
['current_month_rides_count'] ??
'0';
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: cs.primary.withValues(alpha: 0.15)),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
cs.primary.withValues(alpha: 0.20),
cs.primary.withValues(alpha: 0.08),
],
),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: cs.primary.withValues(alpha: 0.25)),
),
child: Icon(Icons.calendar_today_rounded,
color: cs.primary, size: 24),
),
const SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'إجمالي رحلات الشهر',
style: TextStyle(
color: cs.onSurfaceVariant, fontSize: 12),
),
const SizedBox(height: 4),
Text(
'$count',
style: TextStyle(
color: cs.onSurface,
fontSize: 28,
fontWeight: FontWeight.w800,
height: 1,
),
),
],
),
],
),
);
}
Widget _buildRideDetails(RideAdminController controller, ColorScheme cs) {
if (controller.ridesDetails.isEmpty) return const SizedBox.shrink();
final details = controller.ridesDetails[0];
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: cs.outline),
boxShadow: [
BoxShadow(
color: cs.shadow.withValues(alpha: 0.06),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: const Color(0xFFF59E0B).withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: const Color(0xFFF59E0B).withValues(alpha: 0.25)),
),
child: Icon(Icons.analytics_rounded,
color: const Color(0xFFF59E0B), size: 16),
),
const SizedBox(width: 10),
Text(
'تفاصيل الرحلات',
style: TextStyle(
color: cs.onSurface,
fontSize: 14,
fontWeight: FontWeight.w700,
),
),
],
),
const SizedBox(height: 16),
_buildDetailGrid(details, cs),
],
),
);
}
Widget _buildDetailGrid(dynamic details, ColorScheme cs) {
final items = [
_DetailItem('متوسط مدة السائق', details['driver_avg_duration'] ?? '',
Icons.timer_rounded, cs.tertiary),
_DetailItem('عدد السائقين', details['num_Driver'] ?? '',
Icons.people_rounded, cs.primary),
_DetailItem('إجمالي الرحلات', details['total_rides'] ?? '',
Icons.route_rounded, cs.primary),
_DetailItem('رحلات نشطة', details['ongoing_rides'] ?? '',
Icons.play_circle_rounded, const Color(0xFFF59E0B)),
_DetailItem('مكتملة', details['completed_rides'] ?? '',
Icons.check_circle_rounded, const Color(0xFF10B981)),
_DetailItem('ملغاة', details['cancelled_rides'] ?? '',
Icons.cancel_rounded, cs.error),
_DetailItem('أطول مدة', details['longest_duration'] ?? '',
Icons.hourglass_top_rounded, cs.tertiary),
_DetailItem('إجمالي المسافة', '${details['total_distance'] ?? 0} km',
Icons.straighten_rounded, cs.primary),
_DetailItem('متوسط المسافة', '${details['average_distance'] ?? 0} km',
Icons.speed_rounded, const Color(0xFFF59E0B)),
_DetailItem('أطول مسافة', '${details['longest_distance'] ?? 0} km',
Icons.arrow_forward_rounded, cs.tertiary),
_DetailItem('إجمالي أرباح السائقين',
'\$${details['total_driver_earnings'] ?? 0}', Icons.payments_rounded,
const Color(0xFF10B981)),
_DetailItem('إجمالي أرباح الشركة',
'\$${details['total_company_earnings'] ?? 0}',
Icons.business_center_rounded, cs.primary),
_DetailItem('نسبة الشركة', '${details['companyPercent'] ?? 0}%',
Icons.pie_chart_rounded, const Color(0xFFF59E0B)),
];
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 2.2,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: cs.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: item.color.withValues(alpha: 0.15)),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: item.color.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(8),
),
child: Icon(item.icon, color: item.color, size: 16),
),
const SizedBox(width: 8),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.label,
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 10,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
item.value,
style: TextStyle(
color: cs.onSurface,
fontSize: 13,
fontWeight: FontWeight.w700,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
);
},
);
}
}
class _DetailItem {
final String label, value;
final IconData icon;
final Color color;
_DetailItem(this.label, this.value, this.icon, this.color);
}
@@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_admin/constant/colors.dart';
import 'package:siro_admin/controller/admin/security_v2_controller.dart';
class AuditLogsPage extends StatelessWidget {
@@ -8,21 +7,24 @@ class AuditLogsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: AppColor.bg,
backgroundColor: cs.surface,
appBar: AppBar(
title: const Text('سجل العمليات (Audit Logs)',
style: TextStyle(fontWeight: FontWeight.bold)),
backgroundColor: AppColor.surface,
title: Text('سجل العمليات (Audit Logs)',
style: TextStyle(fontWeight: FontWeight.bold, color: cs.onSurface)),
backgroundColor: cs.surface,
elevation: 0,
centerTitle: true,
iconTheme: IconThemeData(color: cs.onSurface),
),
body: GetBuilder<SecurityV2Controller>(
init: SecurityV2Controller(),
builder: (ctrl) {
if (ctrl.isLoading) {
return const Center(
child: CircularProgressIndicator(color: AppColor.accent));
return Center(
child: CircularProgressIndicator(color: cs.primary));
}
if (ctrl.auditLogs.isEmpty) {
@@ -31,15 +33,15 @@ class AuditLogsPage extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.security_rounded,
size: 64, color: AppColor.textSecondary.withOpacity(0.3)),
size: 64, color: cs.onSurfaceVariant.withValues(alpha: 0.3)),
const SizedBox(height: 16),
const Text('لا توجد عمليات مسجلة حالياً',
style: TextStyle(color: AppColor.textSecondary)),
Text('لا توجد عمليات مسجلة حالياً',
style: TextStyle(color: cs.onSurfaceVariant)),
const SizedBox(height: 8),
const Text(
Text(
'تأكد من إنشاء جدول سجل العمليات في قاعدة البيانات',
style: TextStyle(
color: AppColor.textSecondary, fontSize: 10)),
color: cs.onSurfaceVariant, fontSize: 10)),
],
),
);
@@ -50,7 +52,7 @@ class AuditLogsPage extends StatelessWidget {
itemCount: ctrl.auditLogs.length,
itemBuilder: (ctx, i) {
final log = ctrl.auditLogs[i];
return _buildLogItem(log);
return _buildLogItem(log, cs);
},
);
},
@@ -58,14 +60,14 @@ class AuditLogsPage extends StatelessWidget {
);
}
Widget _buildLogItem(Map<String, dynamic> log) {
Widget _buildLogItem(Map<String, dynamic> log, ColorScheme cs) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColor.surface,
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColor.divider),
border: Border.all(color: cs.outline),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -74,22 +76,22 @@ class AuditLogsPage extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(log['admin_name'] ?? 'أدمن غير معروف',
style: const TextStyle(
color: AppColor.accent, fontWeight: FontWeight.bold)),
style: TextStyle(
color: cs.primary, fontWeight: FontWeight.bold)),
Text(log['created_at'] ?? '',
style: const TextStyle(
color: AppColor.textSecondary, fontSize: 11)),
style: TextStyle(
color: cs.onSurfaceVariant, fontSize: 11)),
],
),
const SizedBox(height: 8),
Text(log['action'] ?? '',
style: const TextStyle(
color: AppColor.textPrimary, fontWeight: FontWeight.w600)),
style: TextStyle(
color: cs.onSurface, fontWeight: FontWeight.w600)),
if (log['details'] != null) ...[
const SizedBox(height: 4),
Text(log['details'],
style: const TextStyle(
color: AppColor.textSecondary, fontSize: 12)),
style: TextStyle(
color: cs.onSurfaceVariant, fontSize: 12)),
],
const SizedBox(height: 8),
Row(
@@ -97,19 +99,19 @@ class AuditLogsPage extends StatelessWidget {
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppColor.info.withOpacity(0.1),
color: cs.tertiary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(6),
),
child: Text(log['table_name'] ?? '',
style: const TextStyle(
color: AppColor.info,
style: TextStyle(
color: cs.tertiary,
fontSize: 10,
fontWeight: FontWeight.bold)),
),
const SizedBox(width: 8),
Text('ID: ${log['record_id']}',
style: const TextStyle(
color: AppColor.textSecondary, fontSize: 10)),
style: TextStyle(
color: cs.onSurfaceVariant, fontSize: 10)),
],
),
],
@@ -3,23 +3,20 @@ import 'package:get/get.dart';
import '../../../controller/admin/staff_controller.dart';
class AddStaffPage extends StatelessWidget {
final String role; // 'admin' or 'service'
final String role;
const AddStaffPage({super.key, required this.role});
@override
Widget build(BuildContext context) {
final controller = Get.put(StaffController());
controller.selectedRole = role;
const Color bgColor = Color(0xFF0D1117);
const Color inputColor = Color(0xFF161B22);
const Color accentColor = Color(0xFF00D4AA);
final cs = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: bgColor,
backgroundColor: cs.surface,
appBar: AppBar(
title: Text(role == 'admin' ? "إضافة مدير جديد" : "إضافة موظف خدمة عملاء"),
backgroundColor: bgColor,
backgroundColor: cs.surface,
elevation: 0,
),
body: SingleChildScrollView(
@@ -29,20 +26,20 @@ class AddStaffPage extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionTitle("المعلومات الأساسية"),
_buildSectionTitle("المعلومات الأساسية", cs),
const SizedBox(height: 16),
_buildTextField(
controller: controller.nameController,
label: "الاسم الكامل",
icon: Icons.person_outline,
fillColor: inputColor,
cs: cs,
),
const SizedBox(height: 16),
_buildTextField(
controller: controller.phoneController,
label: "رقم الهاتف",
icon: Icons.phone_android_outlined,
fillColor: inputColor,
cs: cs,
keyboardType: TextInputType.phone,
),
const SizedBox(height: 16),
@@ -50,7 +47,7 @@ class AddStaffPage extends StatelessWidget {
controller: controller.emailController,
label: "البريد الإلكتروني",
icon: Icons.email_outlined,
fillColor: inputColor,
cs: cs,
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 16),
@@ -58,11 +55,11 @@ class AddStaffPage extends StatelessWidget {
controller: controller.passwordController,
label: "كلمة المرور",
icon: Icons.lock_outline,
fillColor: inputColor,
cs: cs,
obscureText: true,
),
const SizedBox(height: 24),
_buildSectionTitle("معلومات إضافية"),
_buildSectionTitle("معلومات إضافية", cs),
const SizedBox(height: 16),
Row(
children: [
@@ -72,7 +69,7 @@ class AddStaffPage extends StatelessWidget {
value: controller.selectedGender,
items: ['Male', 'Female'],
onChanged: (val) => controller.selectedGender = val!,
fillColor: inputColor,
cs: cs,
),
),
const SizedBox(width: 16),
@@ -81,7 +78,7 @@ class AddStaffPage extends StatelessWidget {
controller: controller.birthdateController,
label: "تاريخ الميلاد",
icon: Icons.calendar_today_outlined,
fillColor: inputColor,
cs: cs,
hint: "YYYY-MM-DD",
),
),
@@ -93,7 +90,7 @@ class AddStaffPage extends StatelessWidget {
value: controller.selectedCountry,
items: const ['Jordan', 'Syria', 'Egypt'],
onChanged: (val) => controller.selectedCountry = val!,
fillColor: inputColor,
cs: cs,
),
const SizedBox(height: 40),
GetBuilder<StaffController>(
@@ -103,17 +100,17 @@ class AddStaffPage extends StatelessWidget {
child: ElevatedButton(
onPressed: controller.isLoading ? null : () => controller.registerStaff(),
style: ElevatedButton.styleFrom(
backgroundColor: accentColor,
backgroundColor: cs.primary,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
child: controller.isLoading
? const CircularProgressIndicator(color: Colors.white)
? CircularProgressIndicator(color: cs.onPrimary)
: Text(
"حفظ البيانات",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: bgColor,
color: cs.onPrimary,
),
),
),
@@ -126,11 +123,11 @@ class AddStaffPage extends StatelessWidget {
);
}
Widget _buildSectionTitle(String title) {
Widget _buildSectionTitle(String title, ColorScheme cs) {
return Text(
title,
style: const TextStyle(
color: Color(0xFF7D8590),
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 13,
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
@@ -142,28 +139,28 @@ class AddStaffPage extends StatelessWidget {
required TextEditingController controller,
required String label,
required IconData icon,
required Color fillColor,
required ColorScheme cs,
String? hint,
bool obscureText = false,
TextInputType keyboardType = TextInputType.text,
}) {
return Container(
decoration: BoxDecoration(
color: fillColor,
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white.withOpacity(0.05)),
border: Border.all(color: cs.outline.withValues(alpha: 0.1)),
),
child: TextFormField(
controller: controller,
obscureText: obscureText,
keyboardType: keyboardType,
style: const TextStyle(color: Colors.white),
style: TextStyle(color: cs.onSurface),
decoration: InputDecoration(
labelText: label,
hintText: hint,
hintStyle: const TextStyle(color: Colors.white24),
labelStyle: const TextStyle(color: Colors.white54),
prefixIcon: Icon(icon, color: Colors.white38),
hintStyle: TextStyle(color: cs.onSurface.withValues(alpha: 0.24)),
labelStyle: TextStyle(color: cs.onSurfaceVariant),
prefixIcon: Icon(icon, color: cs.onSurfaceVariant),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
),
@@ -177,22 +174,22 @@ class AddStaffPage extends StatelessWidget {
required String value,
required List<String> items,
required Function(String?) onChanged,
required Color fillColor,
required ColorScheme cs,
}) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: fillColor,
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white.withOpacity(0.05)),
border: Border.all(color: cs.outline.withValues(alpha: 0.1)),
),
child: DropdownButtonFormField<String>(
initialValue: value,
dropdownColor: fillColor,
style: const TextStyle(color: Colors.white),
dropdownColor: cs.surfaceContainerHighest,
style: TextStyle(color: cs.onSurface),
decoration: InputDecoration(
labelText: label,
labelStyle: const TextStyle(color: Colors.white54),
labelStyle: TextStyle(color: cs.onSurfaceVariant),
border: InputBorder.none,
),
items: items.map((e) => DropdownMenuItem(value: e, child: Text(e))).toList(),
@@ -50,7 +50,7 @@ class _PendingAdminsPageState extends State<PendingAdminsPage> {
);
if (response != 'failure') {
mySnackbarSuccess('تم تنفيذ الإجراء بنجاح');
_fetchPendingAdmins(); // تحديث القائمة
_fetchPendingAdmins();
}
} catch (e) {
mySnackbarError('حدث خطأ: $e');
@@ -59,35 +59,37 @@ class _PendingAdminsPageState extends State<PendingAdminsPage> {
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: const Color(0xFF0A0D14),
backgroundColor: cs.surface,
appBar: AppBar(
title: const Text('طلبات الانضمام المعلقة', style: TextStyle(fontWeight: FontWeight.bold)),
backgroundColor: const Color(0xFF161D2E),
backgroundColor: cs.surfaceContainerHighest,
elevation: 0,
),
body: _isLoading
? const Center(child: CircularProgressIndicator(color: Color(0xFF00E5FF)))
? Center(child: CircularProgressIndicator(color: cs.primary))
: _pendingAdmins.isEmpty
? _buildEmptyState()
: _buildList(),
? _buildEmptyState(cs)
: _buildList(cs),
);
}
Widget _buildEmptyState() {
Widget _buildEmptyState(ColorScheme cs) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.person_search_rounded, size: 80, color: Colors.grey[800]),
Icon(Icons.person_search_rounded, size: 80, color: cs.outline),
const SizedBox(height: 16),
const Text('لا توجد طلبات معلقة حالياً', style: TextStyle(color: Colors.grey)),
Text('لا توجد طلبات معلقة حالياً', style: TextStyle(color: cs.onSurfaceVariant)),
],
),
);
}
Widget _buildList() {
Widget _buildList(ColorScheme cs) {
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _pendingAdmins.length,
@@ -97,40 +99,40 @@ class _PendingAdminsPageState extends State<PendingAdminsPage> {
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF161D2E),
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFF1F2D4A)),
border: Border.all(color: cs.outline),
),
child: Column(
children: [
ListTile(
contentPadding: EdgeInsets.zero,
leading: const CircleAvatar(
backgroundColor: Color(0xFF1F2D4A),
child: Icon(Icons.person, color: Color(0xFF00E5FF)),
leading: CircleAvatar(
backgroundColor: cs.outline,
child: Icon(Icons.person, color: cs.primary),
),
title: Text(admin['name'] ?? 'بدون اسم', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
subtitle: Text(admin['phone'] ?? 'بدون رقم', style: const TextStyle(color: Colors.grey)),
title: Text(admin['name'] ?? 'بدون اسم', style: TextStyle(color: cs.onSurface, fontWeight: FontWeight.bold)),
subtitle: Text(admin['phone'] ?? 'بدون رقم', style: TextStyle(color: cs.onSurfaceVariant)),
trailing: Text(
admin['created_at']?.split(' ')[0] ?? '',
style: const TextStyle(color: Colors.grey, fontSize: 12),
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 12),
),
),
const Divider(color: Color(0xFF1F2D4A), height: 24),
Divider(color: cs.outline, height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => _handleAction(admin['id'], 'rejected'),
style: TextButton.styleFrom(foregroundColor: Colors.redAccent),
style: TextButton.styleFrom(foregroundColor: cs.error),
child: const Text('رفض'),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () => _handleAction(admin['id'], 'approved'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00E5FF),
foregroundColor: Colors.black,
backgroundColor: cs.primary,
foregroundColor: cs.onPrimary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
@@ -1,7 +1,6 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_admin/constant/colors.dart';
import 'package:siro_admin/controller/admin/analytics_v2_controller.dart';
class AdvancedAnalyticsPage extends StatelessWidget {
@@ -10,108 +9,250 @@ class AdvancedAnalyticsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final controller = Get.put(AnalyticsV2Controller());
final cs = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: AppColor.bg,
appBar: AppBar(
title: const Text('التحليلات المتقدمة',
style: TextStyle(fontWeight: FontWeight.bold)),
backgroundColor: AppColor.surface,
elevation: 0,
centerTitle: true,
actions: [
IconButton(
icon: const Icon(Icons.refresh_rounded),
onPressed: () => controller.fetchAllAnalytics(),
)
backgroundColor: cs.surface,
body: Column(
children: [
_buildAppBar(context, cs),
Expanded(
child: GetBuilder<AnalyticsV2Controller>(
builder: (ctrl) {
if (ctrl.isLoading) {
return _buildLoadingState(cs);
}
return SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSummarySection(ctrl.revenueData['summary'], cs),
const SizedBox(height: 24),
_buildSectionTitle('إيرادات آخر 30 يوم', cs),
_buildRevenueChart(ctrl.revenueData['daily'] ?? [], cs),
const SizedBox(height: 32),
_buildSectionTitle('نمو المستخدمين (آخر 30 يوم)', cs),
_buildGrowthChart(ctrl.growthData, cs),
const SizedBox(height: 32),
_buildSectionTitle('أفضل 10 سائقين (حسب الرحلات)', cs),
_buildTopDriversList(ctrl.topDrivers, cs),
const SizedBox(height: 40),
],
),
);
},
),
),
],
),
body: GetBuilder<AnalyticsV2Controller>(
builder: (ctrl) {
if (ctrl.isLoading) {
return const Center(
child: CircularProgressIndicator(color: AppColor.accent));
}
);
}
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSummarySection(ctrl.revenueData['summary']),
const SizedBox(height: 24),
_buildSectionTitle('إيرادات آخر 30 يوم'),
_buildRevenueChart(ctrl.revenueData['daily'] ?? []),
const SizedBox(height: 32),
_buildSectionTitle('نمو المستخدمين (آخر 30 يوم)'),
_buildGrowthChart(ctrl.growthData),
const SizedBox(height: 32),
_buildSectionTitle('أفضل 10 سائقين (حسب الرحلات)'),
_buildTopDriversList(ctrl.topDrivers),
const SizedBox(height: 40),
],
Widget _buildAppBar(BuildContext context, ColorScheme cs) {
return Container(
padding: const EdgeInsets.fromLTRB(16, 50, 16, 16),
decoration: BoxDecoration(
color: cs.surface,
border: Border(bottom: BorderSide(color: cs.outline)),
),
child: Row(
children: [
GestureDetector(
onTap: () => Get.back(),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: cs.outline),
),
child: Icon(Icons.arrow_back_ios_new_rounded,
color: cs.onSurfaceVariant, size: 16),
),
);
},
),
const SizedBox(width: 12),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: cs.primary.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: cs.primary.withValues(alpha: 0.25)),
),
child: Icon(Icons.analytics_rounded,
color: cs.primary, size: 18),
),
const SizedBox(width: 10),
Text(
'التحليلات المتقدمة',
style: TextStyle(
color: cs.onSurface,
fontSize: 18,
fontWeight: FontWeight.w700,
),
),
const Spacer(),
GestureDetector(
onTap: () => Get.find<AnalyticsV2Controller>().fetchAllAnalytics(),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: cs.outline),
),
child: Icon(Icons.refresh_rounded,
color: cs.onSurfaceVariant, size: 18),
),
),
],
),
);
}
Widget _buildSectionTitle(String title) {
Widget _buildLoadingState(ColorScheme cs) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 40,
height: 40,
child: CircularProgressIndicator(
color: cs.primary,
strokeWidth: 2,
backgroundColor: cs.primary.withValues(alpha: 0.1),
),
),
const SizedBox(height: 16),
Text('جاري تحميل التحليلات...',
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13)),
],
),
);
}
Widget _buildSectionTitle(String title, ColorScheme cs) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Text(
title,
style: const TextStyle(
color: AppColor.textPrimary,
fontSize: 16,
fontWeight: FontWeight.bold,
),
child: Row(
children: [
Container(
width: 3,
height: 16,
decoration: BoxDecoration(
color: cs.primary,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 10),
Text(
title,
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w700,
),
),
],
),
);
}
Widget _buildSummarySection(Map<String, dynamic>? summary) {
Widget _buildSummarySection(
Map<String, dynamic>? summary, ColorScheme cs) {
if (summary == null) return const SizedBox();
return Row(
children: [
_buildSummaryCard('إجمالي الإيرادات',
'${summary['total_revenue_all'] ?? 0}', AppColor.info),
_buildSummaryCard(
'إجمالي الإيرادات',
'${summary['total_revenue_all'] ?? 0}',
cs.tertiary,
Icons.attach_money_rounded,
cs,
),
const SizedBox(width: 12),
_buildSummaryCard('صافي الربح', '${summary['total_profit_all'] ?? 0}',
AppColor.success),
_buildSummaryCard(
'صافي الربح',
'${summary['total_profit_all'] ?? 0}',
const Color(0xFF10B981),
Icons.trending_up_rounded,
cs,
),
],
);
}
Widget _buildSummaryCard(String title, String value, Color color) {
Widget _buildSummaryCard(
String title, String value, Color color, IconData icon, ColorScheme cs) {
return Expanded(
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColor.surface,
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: color.withOpacity(0.3)),
border: Border.all(color: color.withValues(alpha: 0.2)),
boxShadow: [
BoxShadow(
color: color.withValues(alpha: 0.06),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: const TextStyle(
color: AppColor.textSecondary, fontSize: 12)),
const SizedBox(height: 8),
Text(value,
style: TextStyle(
color: color, fontSize: 22, fontWeight: FontWeight.bold)),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: color.withValues(alpha: 0.25)),
),
child: Icon(icon, color: color, size: 18),
),
const SizedBox(height: 12),
Text(
title,
style: TextStyle(
color: cs.onSurfaceVariant, fontSize: 12),
),
const SizedBox(height: 6),
Text(
value,
style: TextStyle(
color: color,
fontSize: 22,
fontWeight: FontWeight.w800,
height: 1,
),
),
],
),
),
);
}
Widget _buildRevenueChart(List<dynamic> daily) {
if (daily.isEmpty) return const Center(child: Text('لا توجد بيانات'));
Widget _buildRevenueChart(List<dynamic> daily, ColorScheme cs) {
if (daily.isEmpty) {
return Container(
height: 200,
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: cs.outline),
),
child: Center(
child: Text('لا توجد بيانات',
style: TextStyle(color: cs.onSurfaceVariant)),
),
);
}
List<FlSpot> revenueSpots = [];
List<FlSpot> profitSpots = [];
@@ -127,71 +268,131 @@ class AdvancedAnalyticsPage extends StatelessWidget {
height: 300,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(24),
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: cs.outline),
boxShadow: [
BoxShadow(
color: cs.shadow.withValues(alpha: 0.06),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: LineChart(
LineChartData(
gridData: FlGridData(
show: true,
drawVerticalLine: false,
getDrawingHorizontalLine: (v) =>
FlLine(color: AppColor.divider, strokeWidth: 1)),
titlesData: FlTitlesData(
show: true,
rightTitles:
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
topTitles:
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (val, meta) {
if (val.toInt() % 7 == 0 && val.toInt() < daily.length) {
return Text(
daily[val.toInt()]['date'].toString().substring(8),
style: const TextStyle(
color: AppColor.textSecondary, fontSize: 10));
}
return const SizedBox();
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
_LegendDot(color: cs.tertiary, label: 'الإيرادات'),
const SizedBox(width: 16),
_LegendDot(color: const Color(0xFF10B981), label: 'الربح'),
],
),
const SizedBox(height: 16),
Expanded(
child: Directionality(
textDirection: TextDirection.ltr,
child: LineChart(
LineChartData(
gridData: FlGridData(
show: true,
drawVerticalLine: false,
getDrawingHorizontalLine: (v) => FlLine(
color: cs.outline.withValues(alpha: 0.5),
strokeWidth: 1),
),
titlesData: FlTitlesData(
show: true,
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false)),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false)),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 40,
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: 24,
getTitlesWidget: (val, meta) {
if (val.toInt() % 7 == 0 &&
val.toInt() < daily.length) {
return Text(
daily[val.toInt()]['date']
.toString()
.substring(8),
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 9),
);
}
return const SizedBox();
},
),
),
),
borderData: FlBorderData(show: false),
lineBarsData: [
LineChartBarData(
spots: revenueSpots,
isCurved: true,
color: cs.tertiary,
barWidth: 3,
isStrokeCapRound: true,
dotData: const FlDotData(show: false),
belowBarData: BarAreaData(
show: true,
color: cs.tertiary.withValues(alpha: 0.08),
),
),
LineChartBarData(
spots: profitSpots,
isCurved: true,
color: const Color(0xFF10B981),
barWidth: 3,
isStrokeCapRound: true,
dotData: const FlDotData(show: false),
belowBarData: BarAreaData(
show: true,
color: const Color(0xFF10B981).withValues(alpha: 0.08),
),
),
],
),
),
),
),
borderData: FlBorderData(show: false),
lineBarsData: [
LineChartBarData(
spots: revenueSpots,
isCurved: true,
color: AppColor.info,
barWidth: 3,
isStrokeCapRound: true,
dotData: const FlDotData(show: false),
belowBarData: BarAreaData(
show: true, color: AppColor.info.withOpacity(0.1)),
),
LineChartBarData(
spots: profitSpots,
isCurved: true,
color: AppColor.success,
barWidth: 3,
isStrokeCapRound: true,
dotData: const FlDotData(show: false),
belowBarData: BarAreaData(
show: true, color: AppColor.success.withOpacity(0.1)),
),
],
),
],
),
);
}
Widget _buildGrowthChart(Map<String, dynamic> data) {
Widget _buildGrowthChart(Map<String, dynamic> data, ColorScheme cs) {
final passengers = data['passenger_daily'] as List<dynamic>? ?? [];
final drivers = data['driver_daily'] as List<dynamic>? ?? [];
if (passengers.isEmpty && drivers.isEmpty) {
return const Center(child: Text('لا توجد بيانات'));
return Container(
height: 200,
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: cs.outline),
),
child: Center(
child: Text('لا توجد بيانات',
style: TextStyle(color: cs.onSurfaceVariant)),
),
);
}
List<BarChartGroupData> barGroups = [];
@@ -211,59 +412,118 @@ class AdvancedAnalyticsPage extends StatelessWidget {
x: i,
barRods: [
BarChartRodData(
toY: pCount,
color: AppColor.info,
width: 8,
borderRadius: BorderRadius.circular(4)),
toY: pCount,
color: cs.tertiary.withValues(alpha: 0.7),
width: 8,
borderRadius: BorderRadius.circular(4),
),
BarChartRodData(
toY: dCount,
color: AppColor.warning,
width: 8,
borderRadius: BorderRadius.circular(4)),
toY: dCount,
color: const Color(0xFFF59E0B).withValues(alpha: 0.7),
width: 8,
borderRadius: BorderRadius.circular(4),
),
],
),
);
}
return Container(
height: 250,
height: 280,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(24),
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: cs.outline),
boxShadow: [
BoxShadow(
color: cs.shadow.withValues(alpha: 0.06),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: BarChart(
BarChartData(
barGroups: barGroups,
borderData: FlBorderData(show: false),
titlesData: FlTitlesData(
show: true,
rightTitles:
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
topTitles:
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (val, meta) {
if (val.toInt() % 7 == 0 && val.toInt() < passengers.length) {
return Text(
passengers[val.toInt()]['date'].toString().substring(8),
style: const TextStyle(
color: AppColor.textSecondary, fontSize: 10));
}
return const SizedBox();
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
_LegendDot(color: cs.tertiary, label: 'ركاب جدد'),
const SizedBox(width: 16),
_LegendDot(
color: const Color(0xFFF59E0B), label: 'سائقين جدد'),
],
),
const SizedBox(height: 16),
Expanded(
child: Directionality(
textDirection: TextDirection.ltr,
child: BarChart(
BarChartData(
barGroups: barGroups,
borderData: FlBorderData(show: false),
titlesData: FlTitlesData(
show: true,
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false)),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false)),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 40,
getTitlesWidget: (v, _) => Text(
v.toInt().toString(),
style: TextStyle(
color: cs.onSurfaceVariant, fontSize: 9),
),
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 24,
getTitlesWidget: (val, meta) {
if (val.toInt() % 7 == 0 &&
val.toInt() < passengers.length) {
return Text(
passengers[val.toInt()]['date']
.toString()
.substring(8),
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 9),
);
}
return const SizedBox();
},
),
),
),
),
),
),
),
),
],
),
);
}
Widget _buildTopDriversList(List<dynamic> drivers) {
if (drivers.isEmpty) return const Center(child: Text('لا توجد بيانات'));
Widget _buildTopDriversList(List<dynamic> drivers, ColorScheme cs) {
if (drivers.isEmpty) {
return Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: cs.outline),
),
child: Center(
child: Text('لا توجد بيانات',
style: TextStyle(color: cs.onSurfaceVariant)),
),
);
}
return ListView.builder(
shrinkWrap: true,
@@ -271,47 +531,105 @@ class AdvancedAnalyticsPage extends StatelessWidget {
itemCount: drivers.length,
itemBuilder: (ctx, i) {
final d = drivers[i];
final isTop3 = i < 3;
final rankColors = [
const Color(0xFFF59E0B),
cs.onSurfaceVariant,
const Color(0xFFCD7F32),
];
return Container(
margin: const EdgeInsets.only(bottom: 12),
margin: const EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColor.surface,
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isTop3
? rankColors[i].withValues(alpha: 0.2)
: cs.outline,
),
),
child: Row(
children: [
CircleAvatar(
backgroundColor: AppColor.accent.withOpacity(0.1),
child: Text('${i + 1}',
style: const TextStyle(
color: AppColor.accent, fontWeight: FontWeight.bold)),
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: isTop3
? rankColors[i].withValues(alpha: 0.12)
: cs.outline,
shape: BoxShape.circle,
border: Border.all(
color: isTop3
? rankColors[i].withValues(alpha: 0.3)
: Colors.transparent,
),
),
child: Center(
child: Text(
'${i + 1}',
style: TextStyle(
color: isTop3 ? rankColors[i] : cs.onSurfaceVariant,
fontWeight: FontWeight.w800,
fontSize: 14,
),
),
),
),
const SizedBox(width: 16),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${d['first_name']} ${d['last_name']}',
style: const TextStyle(
color: AppColor.textPrimary,
fontWeight: FontWeight.bold)),
Text(d['phone'] ?? '',
style: const TextStyle(
color: AppColor.textSecondary, fontSize: 12)),
Text(
'${d['first_name']} ${d['last_name']}',
style: TextStyle(
color: cs.onSurface,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
const SizedBox(height: 3),
Text(
d['phone'] ?? '',
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 11,
fontFamily: 'monospace',
),
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text('${d['completed_rides']} رحلة',
style: const TextStyle(
color: AppColor.info, fontWeight: FontWeight.bold)),
Text('${d['total_revenue']} ل.س',
style: const TextStyle(
color: AppColor.success,
fontSize: 12,
fontWeight: FontWeight.bold)),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: cs.tertiary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'${d['completed_rides']} رحلة',
style: TextStyle(
color: cs.tertiary,
fontWeight: FontWeight.w700,
fontSize: 12,
),
),
),
const SizedBox(height: 4),
Text(
'${d['total_revenue']} ل.س',
style: TextStyle(
color: const Color(0xFF10B981),
fontSize: 11,
fontWeight: FontWeight.w600,
),
),
],
),
],
@@ -321,3 +639,36 @@ class AdvancedAnalyticsPage extends StatelessWidget {
);
}
}
class _LegendDot extends StatelessWidget {
final Color color;
final String label;
const _LegendDot({required this.color, required this.label});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
),
const SizedBox(width: 6),
Text(
label,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontSize: 11,
fontWeight: FontWeight.w500,
),
),
],
);
}
}
@@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart' hide TextDirection;
import 'package:siro_admin/constant/theme.dart';
import 'package:siro_admin/controller/admin/static_controller.dart';
import 'notes_driver_page.dart';
+240 -66
View File
@@ -1,81 +1,255 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_admin/constant/style.dart';
import 'package:siro_admin/views/widgets/elevated_btn.dart';
import 'package:siro_admin/views/widgets/mycircular.dart';
import '../../../controller/admin/wallet_admin_controller.dart';
import '../../widgets/my_scafold.dart';
import '../../widgets/mycircular.dart';
class Wallet extends StatelessWidget {
Wallet({super.key});
final WalletAdminController walletAdminController =
Get.put(WalletAdminController());
@override
Widget build(BuildContext context) {
return MyScafolld(
title: 'Wallet'.tr,
body: [
GetBuilder<WalletAdminController>(builder: (walletAdminController) {
return Center(
child: walletAdminController.isLoading
? const MyCircularProgressIndicator()
: Column(
children: [
MyElevatedButton(
title: 'Pay to them to banks'.tr,
onPressed: () async {
await walletAdminController.payToBankDriverAll();
}),
SizedBox(
height: Get.height * .8,
child: ListView.builder(
itemCount:
walletAdminController.driversWalletPoints.length,
itemBuilder: (BuildContext context, int index) {
var res = walletAdminController
.driversWalletPoints[index];
final cs = Theme.of(context).colorScheme;
if (res != null && res['name_arabic'] != null) {
return Padding(
padding: const EdgeInsets.all(4.0),
child: Container(
decoration: AppStyle.boxDecoration1,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
'driver name: ${res['name_arabic'].toString()}'),
Text(
'Amount: ${res['total_amount'].toString()}'),
],
),
),
),
);
} else {
return Container(); // Return an empty container if the data is null
}
},
),
)
],
return Scaffold(
backgroundColor: cs.surface,
body: Column(
children: [
_buildAppBar(context, cs),
Expanded(
child: GetBuilder<WalletAdminController>(
builder: (controller) {
if (controller.isLoading) {
return _buildLoadingState(cs);
}
if (controller.driversWalletPoints.isEmpty) {
return _buildEmptyState(cs);
}
return Column(
children: [
_buildPayAllButton(context, controller, cs),
Expanded(
child: ListView.separated(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 80),
physics: const BouncingScrollPhysics(),
itemCount: controller.driversWalletPoints.length,
separatorBuilder: (_, __) => const SizedBox(height: 10),
itemBuilder: (context, index) {
var res = controller.driversWalletPoints[index];
if (res != null && res['name_arabic'] != null) {
return _buildWalletCard(res, cs);
}
return const SizedBox.shrink();
},
),
),
],
);
},
),
),
],
),
);
}
Widget _buildAppBar(BuildContext context, ColorScheme cs) {
return Container(
padding: const EdgeInsets.fromLTRB(16, 50, 16, 16),
decoration: BoxDecoration(
color: cs.surface,
border: Border(bottom: BorderSide(color: cs.outline)),
),
child: Row(
children: [
GestureDetector(
onTap: () => Get.back(),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: cs.outline),
),
child: Icon(Icons.arrow_back_ios_new_rounded,
color: cs.onSurfaceVariant, size: 16),
),
),
const SizedBox(width: 12),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: cs.primary.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: cs.primary.withValues(alpha: 0.25)),
),
child: Icon(Icons.account_balance_wallet_rounded,
color: cs.primary, size: 18),
),
const SizedBox(width: 10),
Text(
'المحافظ المالية',
style: TextStyle(
color: cs.onSurface,
fontSize: 18,
fontWeight: FontWeight.w700,
),
),
const Spacer(),
GestureDetector(
onTap: () async {
walletAdminController.getWalletForEachDriverToPay();
},
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: cs.outline),
),
child: Icon(Icons.refresh_rounded,
color: cs.onSurfaceVariant, size: 18),
),
),
],
),
);
}
Widget _buildLoadingState(ColorScheme cs) {
return const Center(child: MyCircularProgressIndicator());
}
Widget _buildEmptyState(ColorScheme cs) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
shape: BoxShape.circle,
),
child: Icon(Icons.account_balance_wallet_rounded,
size: 48, color: cs.onSurfaceVariant),
),
const SizedBox(height: 16),
Text('لا توجد محافظ مالية',
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 14)),
],
),
);
}
Widget _buildPayAllButton(
BuildContext context, WalletAdminController controller, ColorScheme cs) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
child: SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton.icon(
icon: const Icon(Icons.send_rounded, color: Colors.white, size: 18),
label: Text(
'دفع الجميع للبنوك',
style: const TextStyle(
color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14),
),
style: ElevatedButton.styleFrom(
backgroundColor: cs.primary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14)),
elevation: 0,
),
onPressed: () async {
await controller.payToBankDriverAll();
},
),
),
);
}
Widget _buildWalletCard(dynamic res, ColorScheme cs) {
final String name = res['name_arabic']?.toString() ?? 'بدون اسم';
final String amount = res['total_amount']?.toString() ?? '0';
return Container(
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: cs.outline),
),
child: Padding(
padding: const EdgeInsets.all(14),
child: Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
const Color(0xFF10B981).withValues(alpha: 0.20),
const Color(0xFF10B981).withValues(alpha: 0.08),
],
),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF10B981).withValues(alpha: 0.2)),
),
child: Icon(Icons.person_rounded, color: const Color(0xFF10B981), size: 22),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: TextStyle(
color: cs.onSurface,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
);
})
],
isleading: true,
action: IconButton(
onPressed: () async {
walletAdminController.getWalletForEachDriverToPay();
},
icon: const Icon(
Icons.refresh,
color: Colors.black,
const SizedBox(height: 3),
Text(
'سائق',
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 11,
),
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
amount,
style: TextStyle(
color: const Color(0xFF10B981),
fontWeight: FontWeight.w800,
fontSize: 18,
),
),
Text(
'ل.س',
style: TextStyle(
color: const Color(0xFF10B981).withValues(alpha: 0.6),
fontSize: 10,
fontWeight: FontWeight.w500,
),
),
],
),
],
),
),
);
@@ -6,9 +6,6 @@ import '../../print.dart';
import '../widgets/snackbar.dart';
import 'add_invoice_page.dart';
// نفترض أن هذا الموديل موجود في مشروعك، إذا لم يكن موجوداً يرجى إضافته أو تعديل الاستيراد
// import '../../model/invoice_model.dart';
class InvoiceListPage extends StatefulWidget {
const InvoiceListPage({super.key});
@@ -17,17 +14,11 @@ class InvoiceListPage extends StatefulWidget {
}
class _InvoiceListPageState extends State<InvoiceListPage> {
List<dynamic> invoices = []; // استخدام dynamic لتجنب مشاكل الموديل إذا اختلف
List<dynamic> invoices = [];
int totalCount = 0;
double totalAmount = 0.0;
bool isLoading = true;
// الألوان "الإيجابية" للتصميم الجديد
final Color primaryColor = const Color(0xFF4F46E5); // Indigo
final Color secondaryColor = const Color(0xFF818CF8); // Lighter Indigo
final Color moneyColor = const Color(0xFF059669); // Emerald Green
final Color bgColor = const Color(0xFFF3F4F6); // Light Gray Background
@override
void initState() {
super.initState();
@@ -46,7 +37,7 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
final data = response;
if (mounted) {
setState(() {
invoices = data['data']; // استخدام البيانات مباشرة
invoices = data['data'];
totalCount = int.tryParse(data['summary']['count'].toString()) ?? 0;
totalAmount =
double.tryParse(data['summary']['total'].toString()) ?? 0.0;
@@ -66,6 +57,7 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
}
void _showImageDialog(BuildContext context, String imageUrl) {
final cs = Theme.of(context).colorScheme;
showDialog(
context: context,
builder: (_) => Dialog(
@@ -75,8 +67,9 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
children: [
Container(
decoration: BoxDecoration(
color: Colors.white,
color: cs.surface,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: cs.outline),
),
padding: const EdgeInsets.all(5),
child: ClipRRect(
@@ -93,16 +86,16 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
height: 200,
width: 200,
child: Center(
child: CircularProgressIndicator(color: primaryColor),
child: CircularProgressIndicator(color: cs.primary),
),
);
},
errorBuilder: (context, error, stackTrace) {
return const SizedBox(
return SizedBox(
height: 150,
width: 150,
child: Icon(Icons.broken_image,
size: 60, color: Colors.grey),
child: Icon(Icons.broken_image_rounded,
size: 60, color: cs.onSurfaceVariant),
);
},
),
@@ -112,11 +105,17 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
Positioned(
top: 0,
right: 0,
child: CircleAvatar(
backgroundColor: Colors.white,
child: IconButton(
icon: const Icon(Icons.close, color: Colors.black),
onPressed: () => Navigator.pop(context),
child: GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
shape: BoxShape.circle,
border: Border.all(color: cs.outline),
),
child: Icon(Icons.close_rounded,
color: cs.onSurfaceVariant, size: 18),
),
),
),
@@ -128,17 +127,18 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: bgColor,
// زر عائم بتصميم متدرج
backgroundColor: cs.surface,
floatingActionButton: Container(
decoration: BoxDecoration(
gradient: LinearGradient(colors: [primaryColor, secondaryColor]),
borderRadius: BorderRadius.circular(30),
gradient: LinearGradient(colors: [cs.primary, cs.primary.withValues(alpha: 0.7)]),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: primaryColor.withOpacity(0.4),
blurRadius: 10,
color: cs.primary.withValues(alpha: 0.3),
blurRadius: 12,
offset: const Offset(0, 4))
],
),
@@ -146,32 +146,29 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
onPressed: () => Get.to(() => AddInvoicePage()),
label: const Text('إضافة فاتورة',
style: TextStyle(fontWeight: FontWeight.bold)),
icon: const Icon(Icons.add),
icon: const Icon(Icons.add_rounded),
backgroundColor: Colors.transparent,
elevation: 0,
),
),
body: Column(
children: [
// 1. رأس الصفحة (Header & Summary)
_buildHeader(),
// 2. قائمة الفواتير
_buildHeader(cs),
Expanded(
child: isLoading
? Center(child: CircularProgressIndicator(color: primaryColor))
? _buildLoadingState(cs)
: invoices.isEmpty
? _buildEmptyState()
? _buildEmptyState(cs)
: RefreshIndicator(
onRefresh: fetchInvoices,
color: primaryColor,
color: cs.primary,
child: ListView.builder(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 80),
itemCount: invoices.length,
physics: const BouncingScrollPhysics(),
itemBuilder: (context, index) {
final invoice = invoices[index];
return _buildInvoiceCard(invoice);
return _buildInvoiceCard(invoice, cs);
},
),
),
@@ -181,13 +178,12 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
);
}
// === تصميم الهيدر (رأس الصفحة) ===
Widget _buildHeader() {
Widget _buildHeader(ColorScheme cs) {
return Container(
width: double.infinity,
padding: EdgeInsets.only(
top: MediaQuery.of(context).padding.top + 20,
bottom: 30,
bottom: 24,
left: 20,
right: 20,
),
@@ -195,15 +191,15 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [primaryColor, const Color(0xFF6366F1)],
colors: [cs.primary, cs.primary.withValues(alpha: 0.7)],
),
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(30),
bottomRight: Radius.circular(30),
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24),
),
boxShadow: [
BoxShadow(
color: primaryColor.withOpacity(0.3),
color: cs.primary.withValues(alpha: 0.2),
blurRadius: 20,
offset: const Offset(0, 10),
),
@@ -211,38 +207,41 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
),
child: Column(
children: [
// العنوان وزر الرجوع
Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_back_ios,
color: Colors.white, size: 20),
onPressed: () => Get.back(),
),
const Expanded(
child: Text(
"سجل الفواتير",
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
GestureDetector(
onTap: () => Get.back(),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(Icons.arrow_back_ios_new_rounded,
color: Colors.white, size: 16),
),
),
const SizedBox(width: 40), // للمحاذاة
const Spacer(),
const Text(
"سجل الفواتير",
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w700,
),
),
const Spacer(),
const SizedBox(width: 34),
],
),
const SizedBox(height: 25),
// بطاقات الملخص
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: _buildSummaryItem(
title: "الإجمالي",
value: "${totalAmount.toStringAsFixed(1)} د.أ",
icon: Icons.attach_money,
icon: Icons.attach_money_rounded,
isMoney: true,
),
),
@@ -251,7 +250,7 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
child: _buildSummaryItem(
title: "عدد الفواتير",
value: "$totalCount",
icon: Icons.receipt_long,
icon: Icons.receipt_long_rounded,
isMoney: false,
),
),
@@ -262,17 +261,18 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
);
}
Widget _buildSummaryItem(
{required String title,
required String value,
required IconData icon,
required bool isMoney}) {
Widget _buildSummaryItem({
required String title,
required String value,
required IconData icon,
required bool isMoney,
}) {
return Column(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
color: Colors.white.withValues(alpha: 0.2),
shape: BoxShape.circle,
),
child: Icon(icon, color: Colors.white, size: 20),
@@ -282,21 +282,70 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
value,
style: TextStyle(
color: isMoney ? const Color(0xFFD1FAE5) : Colors.white,
fontSize: 22,
fontWeight: FontWeight.bold,
fontSize: 20,
fontWeight: FontWeight.w800,
),
),
Text(
title,
style: TextStyle(color: Colors.white.withOpacity(0.8), fontSize: 12),
style: const TextStyle(
color: Colors.white, fontSize: 11, fontWeight: FontWeight.w500),
),
],
);
}
// === تصميم بطاقة الفاتورة ===
Widget _buildInvoiceCard(dynamic invoice) {
// استخراج البيانات بأمان
Widget _buildLoadingState(ColorScheme cs) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 40,
height: 40,
child: CircularProgressIndicator(
color: cs.primary,
strokeWidth: 2,
backgroundColor: cs.primary.withValues(alpha: 0.1),
),
),
const SizedBox(height: 16),
Text('جاري تحميل الفواتير...',
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13)),
],
),
);
}
Widget _buildEmptyState(ColorScheme cs) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
shape: BoxShape.circle,
),
child: Icon(Icons.receipt_long_rounded,
size: 48, color: cs.onSurfaceVariant),
),
const SizedBox(height: 16),
Text("لا توجد فواتير حالياً",
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 14)),
const SizedBox(height: 8),
TextButton.icon(
onPressed: fetchInvoices,
icon: const Icon(Icons.refresh_rounded),
label: const Text("تحديث"),
),
],
),
);
}
Widget _buildInvoiceCard(dynamic invoice, ColorScheme cs) {
String name = invoice['name'] ?? 'بدون اسم';
String amount = invoice['amount']?.toString() ?? '0';
String date = invoice['date'] ?? '';
@@ -304,22 +353,16 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
String? imageUrl = invoice['imageLink'];
return Container(
margin: const EdgeInsets.only(bottom: 16),
margin: const EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: cs.outline),
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(20),
borderRadius: BorderRadius.circular(16),
onTap: () {
if (imageUrl != null && imageUrl.isNotEmpty) {
_showImageDialog(context, imageUrl);
@@ -328,96 +371,93 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
}
},
child: Padding(
padding: const EdgeInsets.all(16),
padding: const EdgeInsets.all(14),
child: Row(
children: [
// 1. أيقونة أو صورة مصغرة
Container(
width: 50,
height: 50,
width: 48,
height: 48,
decoration: BoxDecoration(
color: primaryColor.withOpacity(0.08),
borderRadius: BorderRadius.circular(15),
color: cs.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: cs.primary.withValues(alpha: 0.15)),
),
child: imageUrl != null && imageUrl.isNotEmpty
? ClipRRect(
borderRadius: BorderRadius.circular(15),
borderRadius: BorderRadius.circular(14),
child: Image.network(
imageUrl,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) =>
Icon(Icons.receipt, color: primaryColor),
Icon(Icons.receipt_rounded, color: cs.primary, size: 22),
),
)
: Icon(Icons.receipt_outlined, color: primaryColor),
: Icon(Icons.receipt_outlined, color: cs.primary, size: 22),
),
const SizedBox(width: 16),
// 2. التفاصيل
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
name,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Color(0xFF1F2937),
Expanded(
child: Text(
name,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 15,
color: cs.onSurface,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 4),
horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: Colors.grey[100],
color: cs.surface,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: cs.outline),
),
child: Text(
"#$invNumber",
style: TextStyle(
fontSize: 10,
color: Colors.grey[600],
fontWeight: FontWeight.bold),
color: cs.onSurfaceVariant,
fontWeight: FontWeight.w600),
),
),
],
),
const SizedBox(height: 6),
const SizedBox(height: 4),
Text(
date,
style: TextStyle(color: Colors.grey[500], fontSize: 12),
style: TextStyle(
color: cs.onSurfaceVariant, fontSize: 11),
),
],
),
),
const SizedBox(width: 12),
// 3. المبلغ
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
amount,
style: TextStyle(
color: moneyColor,
fontWeight: FontWeight.w900,
color: const Color(0xFF10B981),
fontWeight: FontWeight.w800,
fontSize: 18,
),
),
Text(
"د.أ",
style: TextStyle(
color: moneyColor.withOpacity(0.7),
color: const Color(0xFF10B981).withValues(alpha: 0.6),
fontWeight: FontWeight.w500,
fontSize: 12,
fontSize: 10,
),
),
],
@@ -429,26 +469,4 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
),
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.receipt_long_rounded, size: 80, color: Colors.grey[300]),
const SizedBox(height: 16),
Text(
"لا توجد فواتير حالياً",
style: TextStyle(color: Colors.grey[500], fontSize: 16),
),
const SizedBox(height: 8),
TextButton.icon(
onPressed: fetchInvoices,
icon: const Icon(Icons.refresh),
label: const Text("تحديث"),
),
],
),
);
}
}
+32 -30
View File
@@ -1,8 +1,6 @@
// org_list_page.dart — قائمة مؤسسات مواصلاتي (لوحة إدارة سيرو)
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../constant/colors.dart';
import '../../controller/transit/transit_admin_controller.dart';
import '../../controller/transit/transit_admin_models.dart';
import 'org_create_page.dart';
@@ -27,22 +25,25 @@ class _TransitOrgListPageState extends State<TransitOrgListPage> {
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return GetBuilder<TransitAdminController>(
builder: (c) => Scaffold(
backgroundColor: AppColor.bg,
backgroundColor: cs.surface,
appBar: AppBar(
backgroundColor: AppColor.bg,
backgroundColor: cs.surface,
elevation: 0,
title: const Text('مواصلاتي — المؤسسات', style: TextStyle(color: AppColor.textPrimary)),
title: Text('مواصلاتي — المؤسسات',
style: TextStyle(color: cs.onSurface)),
iconTheme: IconThemeData(color: cs.onSurface),
actions: [
// زر اعتماد الخطوط
IconButton(
icon: const Icon(Icons.route_rounded, color: AppColor.warning),
icon: Icon(Icons.route_rounded, color: cs.tertiary),
tooltip: 'اعتماد الخطوط',
onPressed: () => Get.to(() => const RouteApprovalPage()),
),
IconButton(
icon: const Icon(Icons.add, color: AppColor.accent),
icon: Icon(Icons.add, color: cs.primary),
onPressed: () async {
final created = await Get.to(() => const TransitOrgCreatePage());
if (created == true) c.fetchOrgs();
@@ -56,17 +57,17 @@ class _TransitOrgListPageState extends State<TransitOrgListPage> {
padding: const EdgeInsets.all(12),
child: TextField(
controller: _searchCtrl,
style: const TextStyle(color: AppColor.textPrimary),
style: TextStyle(color: cs.onSurface),
onSubmitted: (v) {
c.searchQuery = v;
c.fetchOrgs();
},
decoration: InputDecoration(
hintText: 'ابحث عن مؤسسة...',
hintStyle: const TextStyle(color: AppColor.textSecondary),
prefixIcon: const Icon(Icons.search, color: AppColor.textSecondary),
hintStyle: TextStyle(color: cs.onSurfaceVariant),
prefixIcon: Icon(Icons.search, color: cs.onSurfaceVariant),
filled: true,
fillColor: AppColor.surface,
fillColor: cs.surfaceContainerHighest,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
@@ -76,17 +77,18 @@ class _TransitOrgListPageState extends State<TransitOrgListPage> {
),
Expanded(
child: c.isLoadingList
? const Center(child: CircularProgressIndicator(color: AppColor.accent))
? Center(child: CircularProgressIndicator(color: cs.primary))
: c.orgs.isEmpty
? const Center(
child: Text('لا توجد مؤسسات', style: TextStyle(color: AppColor.textSecondary)),
? Center(
child: Text('لا توجد مؤسسات',
style: TextStyle(color: cs.onSurfaceVariant)),
)
: RefreshIndicator(
onRefresh: c.fetchOrgs,
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 12),
itemCount: c.orgs.length,
itemBuilder: (_, i) => _orgCard(c.orgs[i]),
itemBuilder: (_, i) => _orgCard(c.orgs[i], cs),
),
),
),
@@ -96,30 +98,30 @@ class _TransitOrgListPageState extends State<TransitOrgListPage> {
);
}
Widget _orgCard(TransitOrgSummary org) {
Widget _orgCard(TransitOrgSummary org, ColorScheme cs) {
Color statusColor;
switch (org.contractStatus) {
case 'active':
statusColor = AppColor.success;
statusColor = const Color(0xFF10B981);
break;
case 'trial':
statusColor = AppColor.info;
statusColor = cs.tertiary;
break;
case 'suspended':
statusColor = AppColor.warning;
statusColor = cs.tertiary;
break;
default:
statusColor = AppColor.danger;
statusColor = cs.error;
}
return Card(
color: AppColor.surface,
color: cs.surfaceContainerHighest,
margin: const EdgeInsets.only(bottom: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
title: Text(org.nameAr,
style: const TextStyle(color: AppColor.textPrimary, fontWeight: FontWeight.bold)),
style: TextStyle(color: cs.onSurface, fontWeight: FontWeight.bold)),
subtitle: Padding(
padding: const EdgeInsets.only(top: 6),
child: Row(
@@ -127,7 +129,7 @@ class _TransitOrgListPageState extends State<TransitOrgListPage> {
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.15),
color: statusColor.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(6),
),
child: Text(org.contractStatus,
@@ -135,21 +137,21 @@ class _TransitOrgListPageState extends State<TransitOrgListPage> {
),
const SizedBox(width: 8),
Text('${org.city} · ${org.country}',
style: const TextStyle(color: AppColor.textSecondary, fontSize: 12)),
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 12)),
const Spacer(),
Icon(Icons.directions_bus, size: 14, color: AppColor.textSecondary),
Icon(Icons.directions_bus, size: 14, color: cs.onSurfaceVariant),
const SizedBox(width: 2),
Text('${org.vehiclesCount}',
style: const TextStyle(color: AppColor.textSecondary, fontSize: 12)),
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 12)),
const SizedBox(width: 10),
Icon(Icons.people, size: 14, color: AppColor.textSecondary),
Icon(Icons.people, size: 14, color: cs.onSurfaceVariant),
const SizedBox(width: 2),
Text('${org.activeEnrollments}',
style: const TextStyle(color: AppColor.textSecondary, fontSize: 12)),
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 12)),
],
),
),
trailing: const Icon(Icons.arrow_forward_ios, size: 14, color: AppColor.textSecondary),
trailing: Icon(Icons.arrow_forward_ios, size: 14, color: cs.onSurfaceVariant),
onTap: () => Get.to(() => TransitOrgDetailsPage(orgId: org.id, orgName: org.nameAr)),
),
);
@@ -1,13 +1,11 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_admin/constant/theme.dart';
import 'package:siro_admin/main.dart';
import 'package:siro_admin/constant/box_name.dart';
import 'package:siro_admin/views/admin/captain/captain.dart';
import 'package:siro_admin/views/admin/passenger/passenger.dart';
import 'package:siro_admin/views/admin/drivers/driver_tracker_screen.dart';
import 'package:siro_admin/views/transit/org_list_page.dart';
import 'package:siro_admin/views/admin/rides/rides.dart';
import 'package:siro_admin/views/admin/static/static.dart';
import 'package:siro_admin/controller/admin/static_controller.dart';
import 'package:siro_admin/views/admin/drivers/monitor_ride.dart';