first commit

This commit is contained in:
Hamza-Ayed
2026-06-09 08:40:31 +03:00
commit d8901e1a87
3161 changed files with 536187 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,335 @@
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 'captain_details.dart';
class CaptainsPage extends StatelessWidget {
CaptainsPage({super.key});
final CaptainAdminController captainController =
Get.put(CaptainAdminController());
final TextEditingController searchController = TextEditingController();
String myPhone = box.read(BoxName.adminPhone).toString();
bool isSuperAdmin = false;
@override
Widget build(BuildContext context) {
isSuperAdmin = myPhone == '963942542053' || myPhone == '963992952235';
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,
],
),
),
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) {
return Container(
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey[200]!),
),
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),
),
onSubmitted: (_) => _performSearch(),
),
),
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),
),
),
),
),
],
),
);
}
void _performSearch() {
final phone = searchController.text.trim();
if (phone.isNotEmpty) {
captainController.find_driver_by_phone(phone);
}
}
// ================= RESULTS =================
Widget _buildResultsList(BuildContext context, List<dynamic> captains) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 12),
child: Row(
children: [
Text(
'Search Results'.tr,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'${captains.length}',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
),
),
),
],
),
),
Expanded(
child: ListView.separated(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.fromLTRB(20, 0, 20, 80),
itemCount: captains.length,
separatorBuilder: (_, __) => const SizedBox(height: 12),
itemBuilder: (context, index) {
final captain = captains[index] as Map<String, dynamic>;
return _buildModernCaptainCard(context, captain);
},
),
),
],
);
}
// ================= CARD =================
Widget _buildModernCaptainCard(
BuildContext context, Map<String, dynamic> captain) {
final String fullName =
'${captain['first_name'] ?? ''} ${captain['last_name'] ?? ''}';
final String phone = captain['phone']?.toString() ?? '';
final String? email = captain['email']?.toString();
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),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () {
Get.to(() => const CaptainDetailsPage(),
arguments: {'data': captain});
},
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context).primaryColor.withOpacity(0.8),
Theme.of(context).primaryColor,
],
),
borderRadius: BorderRadius.circular(16),
),
child: const Icon(
Icons.person_rounded,
color: Colors.white,
size: 28,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
fullName,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 6),
Text(phone),
if (isSuperAdmin && email != null) ...[
const SizedBox(height: 4),
Text(email),
],
],
),
),
],
),
),
),
),
);
}
// ================= STATES =================
Widget _buildLoadingState() {
return const Center(child: MyCircularProgressIndicator());
}
Widget _buildEmptyState() {
return const Center(
child: Text("No captains found"),
);
}
}

View File

@@ -0,0 +1,425 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../constant/box_name.dart';
import '../../../constant/colors.dart';
import '../../../controller/admin/captain_admin_controller.dart';
import '../../../controller/firebase/firbase_messge.dart';
import '../../../main.dart'; // Import main to access myPhone
import '../../widgets/elevated_btn.dart';
import '../../widgets/my_scafold.dart';
import '../../widgets/my_textField.dart';
import '../quality/driver_scorecard_page.dart';
import 'form_captain.dart';
class CaptainDetailsPage extends StatelessWidget {
const CaptainDetailsPage({super.key});
@override
Widget build(BuildContext context) {
final Map<String, dynamic> data = Get.arguments['data'];
final controller = Get.find<CaptainAdminController>();
String myPhone = box.read(BoxName.adminPhone).toString();
// Define Super Admin Logic
final bool isSuperAdmin =
myPhone == '963942542053' || myPhone == '963992952235';
return MyScafolld(
title: 'Captain Profile'.tr,
isleading: true,
body: [
SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 40),
child: Column(
children: [
// --- Header Section (Avatar & Name) ---
_buildHeaderSection(context, data),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: [
// --- Personal Information Card ---
_buildInfoCard(
title: 'Personal Information',
icon: Icons.person,
children: [
_buildDetailTile(
Icons.email_outlined,
'Email',
isSuperAdmin
? data['email']
: _maskEmail(
data['email']) // Mask email for non-super
),
_buildDetailTile(
Icons.phone_iphone,
'Phone',
_formatPhoneNumber(
data['phone'].toString(), isSuperAdmin)),
_buildDetailTile(Icons.transgender, 'Gender',
data['gender'] ?? 'Not specified'),
_buildDetailTile(Icons.cake_outlined, 'Birthdate',
data['birthdate'] ?? 'N/A'),
],
),
const SizedBox(height: 16),
// --- Ride Statistics Card ---
_buildInfoCard(
title: 'Performance & Stats',
icon: Icons.bar_chart_rounded,
children: [
_buildDetailTile(Icons.star_rate_rounded, 'Rating',
'${data['ratingPassenger'] ?? 0.0} / 5.0',
valueColor: Colors.amber[700]),
_buildDetailTile(Icons.directions_car_filled_outlined,
'Total Rides', data['countPassengerRide']),
_buildDetailTile(Icons.cancel_outlined,
'Canceled Rides', data['countPassengerCancel'],
valueColor: Colors.redAccent),
],
),
const SizedBox(height: 30),
// --- Action Buttons ---
_buildActionButtons(
context, controller, data, isSuperAdmin),
],
),
),
],
),
),
],
);
}
// --- Header with Gradient Background ---
Widget _buildHeaderSection(BuildContext context, Map<String, dynamic> data) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 25),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 5),
)
],
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(30)),
),
child: Column(
children: [
CircleAvatar(
radius: 45,
backgroundColor: AppColor.primaryColor.withOpacity(0.1),
child: Text(
data['first_name'] != null
? data['first_name'][0].toUpperCase()
: 'C',
style: TextStyle(
fontSize: 35,
fontWeight: FontWeight.bold,
color: AppColor.primaryColor),
),
),
const SizedBox(height: 12),
Text(
'${data['first_name']} ${data['last_name']}',
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.black87),
),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
),
child: Text(
'Active Captain'.tr,
style: const TextStyle(
fontSize: 12,
color: Colors.green,
fontWeight: FontWeight.w600),
),
),
],
),
);
}
Widget _buildInfoCard(
{required String title,
required IconData icon,
required List<Widget> children}) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.05),
spreadRadius: 2,
blurRadius: 10)
],
border: Border.all(color: Colors.grey.withOpacity(0.1)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, color: AppColor.primaryColor, size: 22),
const SizedBox(width: 10),
Text(title.tr,
style: const TextStyle(
fontSize: 17, fontWeight: FontWeight.bold)),
],
),
Divider(height: 25, color: Colors.grey.withOpacity(0.2)),
...children,
],
),
);
}
Widget _buildDetailTile(IconData icon, String label, dynamic value,
{Color? valueColor}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8)),
child: Icon(icon, color: Colors.grey[600], size: 18),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label.tr,
style: TextStyle(fontSize: 12, color: Colors.grey[500])),
Text(
value?.toString() ?? 'N/A',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: valueColor ?? Colors.black87),
),
],
),
),
],
),
);
}
Widget _buildActionButtons(
BuildContext context,
CaptainAdminController controller,
Map<String, dynamic> data,
bool isSuperAdmin) {
return Column(
children: [
// Driver Scorecard Button
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton.icon(
icon: const Icon(Icons.analytics_outlined, color: Colors.white),
label: Text("بطاقة الأداء (Scorecard)",
style: const TextStyle(color: Colors.white, fontSize: 16)),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueAccent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
onPressed: () {
Get.to(() => DriverScorecardPage(driverId: data['id'].toString()));
},
),
),
const SizedBox(height: 16),
// Notification is available for everyone
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton.icon(
icon: const Icon(Icons.notifications_active_outlined,
color: Colors.white),
label: Text("Send Notification".tr,
style: const TextStyle(color: Colors.white, fontSize: 16)),
style: ElevatedButton.styleFrom(
backgroundColor: AppColor.primaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
onPressed: () => _showSendNotificationDialog(controller, data),
),
),
// Edit and Delete ONLY for Super Admin
if (isSuperAdmin) ...[
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
icon: const Icon(Icons.edit_note_rounded, size: 20),
label: Text("Edit".tr),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: AppColor.yellowColor,
elevation: 0,
side: BorderSide(color: AppColor.yellowColor),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(vertical: 12),
),
onPressed: () {
Get.to(() => const FormCaptain(), arguments: {
'isEditMode': true,
'captainData': data,
});
},
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton.icon(
icon: const Icon(Icons.delete_outline_rounded, size: 20),
label: Text("Delete".tr),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red[50],
foregroundColor: Colors.red,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(vertical: 12),
),
onPressed: () => _showDeleteConfirmation(data),
),
),
],
),
] else ...[
// Message for normal admins
const SizedBox(height: 15),
Text(
"Only Super Admins can edit or delete captains.",
style: TextStyle(
color: Colors.grey[400],
fontSize: 12,
fontStyle: FontStyle.italic),
)
]
],
);
}
// --- Helper Methods ---
String _formatPhoneNumber(String phone, bool isSuperAdmin) {
if (isSuperAdmin) return phone;
if (phone.length <= 4) return phone;
return '${'*' * (phone.length - 4)}${phone.substring(phone.length - 4)}';
}
String _maskEmail(String? email) {
if (email == null || email.isEmpty) return 'N/A';
int atIndex = email.indexOf('@');
if (atIndex <= 1) return email; // Too short to mask
return '${email.substring(0, 2)}****${email.substring(atIndex)}';
}
void _showSendNotificationDialog(
CaptainAdminController controller, Map<String, dynamic> data) {
Get.defaultDialog(
title: 'Send Notification'.tr,
titleStyle: const TextStyle(fontWeight: FontWeight.bold),
content: Form(
key: controller.formCaptainPrizeKey,
child: Column(
children: [
MyTextForm(
controller: controller.titleNotify,
label: 'Title'.tr,
hint: 'Enter notification title'.tr,
type: TextInputType.text,
),
const SizedBox(height: 10),
MyTextForm(
controller: controller.bodyNotify,
label: 'Body'.tr,
hint: 'Enter message body'.tr,
type: TextInputType.text,
),
],
),
),
confirm: SizedBox(
width: 100,
child: MyElevatedButton(
title: 'Send',
onPressed: () {
// Check if key is valid (might be recreated)
if (controller.formCaptainPrizeKey.currentState?.validate() ??
true) {
FirebaseMessagesController().sendNotificationToAnyWithoutData(
controller.titleNotify.text,
controller.bodyNotify.text,
data['passengerToken'] ?? '', // Safety check
'order.wav');
Get.back();
Get.snackbar("Success", "Notification Sent",
backgroundColor: Colors.green.withOpacity(0.2));
}
},
),
),
cancel: TextButton(
onPressed: () => Get.back(),
child: Text('Cancel'.tr, style: const TextStyle(color: Colors.grey))),
);
}
void _showDeleteConfirmation(Map<String, dynamic> user) {
Get.defaultDialog(
title: 'Confirm Deletion'.tr,
titleStyle:
const TextStyle(color: Colors.redAccent, fontWeight: FontWeight.bold),
middleText:
'Are you sure you want to delete ${user['first_name']}? This action cannot be undone.'
.tr,
confirm: ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.redAccent),
onPressed: () {
// Call delete function here
// controller.deleteCaptain(user['id']);
Get.back();
Get.snackbar("Deleted", "Captain has been removed",
backgroundColor: Colors.red.withOpacity(0.2));
},
child: Text('Delete'.tr, style: const TextStyle(color: Colors.white)),
),
cancel: TextButton(
onPressed: () => Get.back(),
child: Text('Cancel'.tr, style: const TextStyle(color: Colors.grey))),
);
}
}

View File

@@ -0,0 +1,81 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../controller/drivers/driver_not_active_controller.dart';
class DriverDetailsPage extends StatelessWidget {
final String driverId;
final DriverController controller = Get.find();
DriverDetailsPage({super.key, required this.driverId});
/// Helper function to safely get String values from dynamic map
String safeVal(Map d, String key) {
final v = d[key];
if (v == null || v == false) return '';
return v.toString();
}
@override
Widget build(BuildContext context) {
controller.getDriverDetails(driverId);
return Scaffold(
appBar: AppBar(title: const Text("Driver Details")),
body: GetBuilder<DriverController>(
id: 'driverDetails',
builder: (c) {
if (c.driverDetails.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
final d = c.driverDetails['driver'] as Map;
final docs = c.driverDetails['documents'] as List;
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Name: ${safeVal(d, 'first_name')} ${safeVal(d, 'last_name')}"),
Text("Phone: ${safeVal(d, 'phone')}"),
Text("Email: ${safeVal(d, 'email')}"),
Text("National Number: ${safeVal(d, 'national_number')}"),
Text("Gender: ${safeVal(d, 'gender')}"),
Text("Birthdate: ${safeVal(d, 'birthdate')}"),
Text("Status: ${safeVal(d, 'status')}"),
Text("License Type: ${safeVal(d, 'license_type')}"),
Text("License Categories: ${safeVal(d, 'license_categories')}"),
Text("Issue Date: ${safeVal(d, 'issue_date')}"),
Text("Expiry Date: ${safeVal(d, 'expiry_date')}"),
Text("Address: ${safeVal(d, 'address')}"),
Text("Site: ${safeVal(d, 'site')}"),
Text("Employment Type: ${safeVal(d, 'employmentType')}"),
Text("Marital Status: ${safeVal(d, 'maritalStatus')}"),
const SizedBox(height: 16),
const Text("Documents:",
style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
...docs.map((doc) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(safeVal(doc, "doc_type")),
const SizedBox(height: 4),
Image.network(
safeVal(doc, "link"),
height: 200,
fit: BoxFit.contain,
errorBuilder: (ctx, err, st) =>
const Icon(Icons.broken_image),
),
const SizedBox(height: 16),
],
)),
],
),
);
},
),
);
}
}

View File

@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_admin/views/admin/captain/register_captain.dart';
import 'package:siro_admin/views/widgets/my_scafold.dart';
import '../../../constant/colors.dart';
import '../../../controller/admin/register_captain_controller.dart';
class DriversCantRegister extends StatelessWidget {
const DriversCantRegister({super.key});
@override
Widget build(BuildContext context) {
Get.put(RegisterCaptainController());
return MyScafolld(
title: 'drivers cant register'.tr,
body: [
GetBuilder<RegisterCaptainController>(builder: (mainController) {
return ListView.builder(
itemCount: mainController.driverNotCompleteRegistration.length,
itemBuilder: (context, index) {
final driver =
mainController.driverNotCompleteRegistration[index];
return Padding(
padding: const EdgeInsets.all(8.0),
child: InkWell(
onTap: () {
Get.to(() => RegisterCaptain(), arguments: {
"phone_number": driver['phone_number'].toString(),
'driverId': driver['driverId'].toString(),
'email': driver['email'].toString(),
});
},
child: Container(
color: driver['note'] == null
? AppColor.greenColor
: AppColor.accentColor,
child: Column(
children: [
Text(driver['phone_number'].toString()),
Text(driver['driverId'].toString()),
Text(driver['email'].toString()),
],
),
),
),
);
},
);
}),
],
isleading: true);
}
}

View File

@@ -0,0 +1,122 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_admin/controller/functions/crud.dart';
import '../../../constant/links.dart';
import '../../../constant/style.dart';
import '../../../controller/admin/captain_admin_controller.dart';
import '../../widgets/elevated_btn.dart';
import '../../widgets/my_scafold.dart';
import '../../widgets/my_textField.dart';
class FormCaptain extends StatefulWidget {
const FormCaptain({super.key});
@override
State<FormCaptain> createState() => _FormCaptainState();
}
class _FormCaptainState extends State<FormCaptain> {
final CaptainAdminController controller = Get.find();
final _formKey = GlobalKey<FormState>();
late TextEditingController firstNameController;
late TextEditingController lastNameController;
late TextEditingController phoneController;
bool isEditMode = false;
Map<String, dynamic>? captainData;
@override
void initState() {
super.initState();
if (Get.arguments != null && Get.arguments['isEditMode'] == true) {
isEditMode = true;
captainData = Get.arguments['captainData'];
firstNameController =
TextEditingController(text: captainData?['first_name'] ?? '');
lastNameController =
TextEditingController(text: captainData?['last_name'] ?? '');
phoneController =
TextEditingController(text: captainData?['phone'] ?? '');
} else {
firstNameController = TextEditingController();
lastNameController = TextEditingController();
phoneController = TextEditingController();
}
}
@override
void dispose() {
firstNameController.dispose();
lastNameController.dispose();
phoneController.dispose();
super.dispose();
}
Future<void> _saveForm() async {
if (_formKey.currentState!.validate()) {
// Create a map of the updated data
Map<String, dynamic> updatedData = {
'id': captainData?['id'], // Important for the WHERE clause in SQL
// 'first_name': firstNameController.text,
// 'last_name': lastNameController.text,
'phone': phoneController.text,
};
var res = await CRUD()
.post(link: AppLink.updateDriverFromAdmin, payload: updatedData);
if (res != 'failure') {
print('Updating data: $updatedData');
Get.back(); // Go back after saving
Get.snackbar('Success', 'Captain data updated successfully!');
}
// controller.updateCaptain(updatedData);
}
}
@override
Widget build(BuildContext context) {
return MyScafolld(
title: 'Edit Captain'.tr,
isleading: true,
body: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: ListView(
children: [
MyTextForm(
controller: firstNameController,
label: 'First Name'.tr,
hint: 'Enter first name'.tr,
type: TextInputType.name,
),
const SizedBox(height: 16),
MyTextForm(
controller: lastNameController,
label: 'Last Name'.tr,
hint: 'Enter last name'.tr,
type: TextInputType.name,
),
const SizedBox(height: 16),
MyTextForm(
controller: phoneController,
label: 'Phone Number'.tr,
hint: 'Enter phone number'.tr,
type: TextInputType.phone,
),
const SizedBox(height: 32),
MyElevatedButton(
title: 'Update'.tr,
onPressed: _saveForm,
),
],
),
),
),
],
);
}
}

View File

@@ -0,0 +1,982 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_admin/controller/admin/register_captain_controller.dart';
import '../../../../constant/colors.dart';
import '../../../../constant/links.dart';
import '../../../../constant/style.dart';
import '../../widgets/elevated_btn.dart';
import '../../widgets/my_scafold.dart';
import '../../widgets/mycircular.dart';
class RegisterCaptain extends StatelessWidget {
RegisterCaptain({super.key});
@override
Widget build(BuildContext context) {
final controller = Get.put(RegisterCaptainController());
// String text = '';
controller.driveInit();
return MyScafolld(
title: 'Documents check'.tr,
action: GetBuilder<RegisterCaptainController>(builder: (controller) {
return IconButton(
onPressed: () {
controller.isLoading = false;
controller.update();
},
icon: const Icon(Icons.refresh),
);
}),
body: [
GetBuilder<RegisterCaptainController>(builder: (controller) {
return controller.isLoading
? const MyCircularProgressIndicator()
: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
(controller.responseIdCardDriverEgyptBack.isNotEmpty &&
controller.responseIdCardDriverEgyptFront
.isNotEmpty &&
controller.responseIdEgyptFront.isNotEmpty &&
controller.responseIdEgyptBack.isNotEmpty &&
controller
.responseIdEgyptDriverLicense.isNotEmpty
// &&
// controller
// .responseCriminalRecordEgypt.isNotEmpty
)
? MyElevatedButton(
title: 'Next'.tr,
onPressed: () {
controller.addDriverAndCarEgypt();
})
: const SizedBox(),
SizedBox(
height:
(controller.responseIdCardDriverEgyptBack
.isNotEmpty &&
controller.responseIdCardDriverEgyptFront
.isNotEmpty &&
controller
.responseIdEgyptFront.isNotEmpty &&
controller
.responseIdEgyptBack.isNotEmpty &&
controller.responseIdEgyptDriverLicense
.isNotEmpty
// &&
// controller.responseCriminalRecordEgypt
// .isNotEmpty
)
? Get.height * .7
: Get.height * .85,
child: ListView(
children: [
egyptDriverLicense(),
egyptCarLicenceFront(),
egyptCarLicenceBack(),
egyptDriverIDFront(),
egyptDriverIDBack(),
// egyptCriminalRecord(),
],
),
),
],
),
);
}),
],
isleading: true);
}
GetBuilder<RegisterCaptainController> egyptDriverLicense() {
return GetBuilder<RegisterCaptainController>(
builder: (ai) {
if (ai.responseIdEgyptDriverLicense.isNotEmpty) {
final expiryDate = ai.responseIdEgyptDriverLicense['expiry_date'];
// Check if the expiry date is before today
final today = DateTime.now();
// Try parsing the expiry date. If it fails, set it to null.
final expiryDateTime = DateTime.tryParse(expiryDate);
final isExpired =
expiryDateTime != null && expiryDateTime.isBefore(today);
return Card(
elevation: 4.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.0),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Driver\'s License'.tr, style: AppStyle.headTitle2),
IconButton(
onPressed: () async {
await ai.allMethodForAI("""
Write a JSON object from the following information extracted from the provided Arabic text:
{
"license_type": "",
"national_number": "",
"name_arabic": "",
"name_english": "",
"firstName": "",
"lastName": "",
"address": "",
"issue_date": "", // Format: YYYY-MM-DD using Latin numerals (0-9)
"expiry_date": "", // Format: YYYY-MM-DD using Latin numerals (0-9)
"employmentType": "",
"license_categories": []
}
Important notes:
1. Ensure all dates are in the format YYYY-MM-DD using Latin (Western) numerals (0-9), not Arabic numerals.
2. The 'license_categories' should be an array, even if there's only one category.
3. Fill in all fields based on the information provided in the Arabic text.
4. If any information is missing, leave the field as an empty string or empty array as appropriate.
""", 'driver_license', ai.driverId); //egypt
},
icon: const Icon(Icons.refresh),
),
],
),
const SizedBox(height: 8.0),
const Divider(color: AppColor.accentColor),
const SizedBox(height: 8.0),
Text(
'${'License Type'.tr}: ${ai.responseIdEgyptDriverLicense['license_type']}',
style: AppStyle.title,
),
const SizedBox(height: 8.0),
Text(
'${'National Number'.tr}: ${ai.responseIdEgyptDriverLicense['national_number']}',
style: AppStyle.title.copyWith(
color: ai.responseIdEgyptDriverLicense[
'national_number'] ==
ai.responseIdEgyptBack['nationalID']
? AppColor.greenColor
: AppColor.redColor),
),
const SizedBox(height: 8.0),
Text(
'${'Name (Arabic)'.tr}: ${ai.responseIdEgyptDriverLicense['name_arabic']}',
),
const SizedBox(height: 8.0),
Text(
'${'Name (English)'.tr}: ${ai.responseIdEgyptDriverLicense['name_english']}',
),
const SizedBox(height: 8.0),
Text(
'${'Address'.tr}: ${ai.responseIdEgyptDriverLicense['address']}',
),
const SizedBox(height: 8.0),
Text(
'${'Issue Date'.tr}: ${ai.responseIdEgyptDriverLicense['issue_date']}',
),
const SizedBox(height: 8.0),
Text(
'${'Expiry Date'.tr}: ${ai.responseIdEgyptDriverLicense['expiry_date']}',
style: AppStyle.title.copyWith(
color:
!isExpired ? AppColor.greenColor : AppColor.redColor,
),
),
const SizedBox(height: 8.0),
Text(
'${'License Categories'.tr}: ${ai.responseIdEgyptDriverLicense['license_categories']}',
),
],
),
),
);
}
return Card(
child: InkWell(
onTap: () async {
await ai.allMethodForAI("""
Write a JSON object from the following information extracted from the provided Arabic text:
{
"license_type": "",
"national_number": "",
"name_arabic": "",
"name_english": "",
"firstName": "",
"lastName": "",
"address": "",
"issue_date": "", // Format: YYYY-MM-DD using Latin numerals (0-9)
"expiry_date": "", // Format: YYYY-MM-DD using Latin numerals (0-9)
"employmentType": "",
"license_categories": []
}
Important notes:
1. Ensure all dates are in the format YYYY-MM-DD using Latin (Western) numerals (0-9), not Arabic numerals.
2. The 'license_categories' should be an array, even if there's only one category.
3. Fill in all fields based on the information provided in the Arabic text.
4. If any information is missing, leave the field as an empty string or empty array as appropriate.
""", 'driver_license', ai.driverId); //egypt
},
child: Column(
children: [
Image.network(
'${AppLink.server}/card_image/driver_license-${ai.driverId}.jpg',
height: Get.height * .25,
width: double.maxFinite,
fit: BoxFit.fitHeight,
),
Text(
'Capture an Image of Your Driver License'.tr,
style: AppStyle.title,
),
],
),
),
);
},
);
}
GetBuilder<RegisterCaptainController> egyptDriverIDBack() {
return GetBuilder<RegisterCaptainController>(
builder: (ai) {
if (ai.responseIdEgyptBack.isNotEmpty) {
final taxExpiryDate = ai.responseIdEgyptBack['expirationDate'];
// Check if the tax expiry date is before today
final today = DateTime.now();
// Try parsing the tax expiry date. If it fails, set it to null.
final taxExpiryDateTime = DateTime.tryParse(taxExpiryDate);
final isExpired =
taxExpiryDateTime != null && taxExpiryDateTime.isBefore(today);
return Card(
elevation: 4.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.0),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('ID Documents Back'.tr, style: AppStyle.headTitle2),
IconButton(
onPressed: () async {
await ai.allMethodForAI("""
Write a JSON from the following information extracted from the provided Arabic text:
- nationalID(in Latin numerals)
- issueDate (in format YYYY-MM-DD using Latin numerals)
- occupation
- gender
- religion
- maritalStatus
- fullNameMarital (if maritalStatus is "أعزب", set this to "none")
- expirationDate (in format YYYY-MM-DD using Latin numerals)
Please ensure all date fields use Latin (Western) numerals (0-9) instead of Arabic numerals. For example, use "2023-04-03" instead of "٢٠٢٣-٠٤-٠٣".
""", 'id_back', ai.driverId); //egypt
},
icon: const Icon(Icons.refresh),
),
],
),
const SizedBox(height: 8.0),
const Divider(color: AppColor.accentColor),
const SizedBox(height: 8.0),
// Assuming these keys exist in ai.responseIdEgyptFront
Text(
'${'National ID'.tr}: ${ai.responseIdEgyptBack['nationalID']}',
style: AppStyle.title.copyWith(
color: ai.responseIdEgyptDriverLicense[
'national_number'] ==
ai.responseIdEgyptBack['nationalID']
? AppColor.greenColor
: AppColor.redColor),
),
const SizedBox(height: 8.0),
Text(
'${'Occupation'.tr}: ${ai.responseIdEgyptBack['occupation']}', // Assuming 'occupation' exists
),
const SizedBox(height: 8.0),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${'Issue Date'.tr}: ${ai.responseIdEgyptBack['issueDate']}', // Assuming 'issueDate' exists
),
Text(
'${'Gender'.tr}: ${ai.responseIdEgyptBack['gender']}', // Assuming 'gender' exists
),
],
),
const SizedBox(height: 8.0),
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Text(
// '${'Religion'.tr}: ${ai.responseIdEgyptBack['religion']}', // Assuming 'religion' exists
// ),
// Text(
// '${'Marital Status'.tr}: ${ai.responseIdEgyptBack['maritalStatus']}', // Assuming 'maritalStatus' exists
// ),
// ],
// ),
// const SizedBox(height: 8.0),
// Text(
// '${'Full Name (Marital)'.tr}: ${ai.responseIdEgyptBack['fullNameMaritial']}', // Assuming 'fullNameMaritial' exists
// ),
// const SizedBox(height: 8.0),
Text(
'${'Expiration Date'.tr}: ${ai.responseIdEgyptBack['expirationDate']}', // Assuming 'expirationDate' exists
style: AppStyle.title.copyWith(
color: !isExpired
? AppColor.greenColor
: AppColor.redColor),
),
],
),
),
);
}
return Card(
child: InkWell(
onTap: () async {
await ai.allMethodForAI('''
Write a JSON object from the following information extracted from the provided Arabic text:
{
"nationalID": "",//(in Latin numerals)
"issueDate": "", // Format: YYYY-MM-DD using Latin numerals (0-9)
"occupation": "",
"gender": "",
"religion": "",
"maritalStatus": "",
"fullNameMaritial": "", // Set to "none" if maritalStatus is "أعزب"
"expirationDate": "" // Format: YYYY-MM-DD using Latin numerals (0-9)
}
Important notes:
1. Ensure all dates (issueDate and expirationDate) are in the format YYYY-MM-DD using Latin (Western) numerals (0-9), not Arabic numerals.
2. If maritalStatus is "أعزب" (single), set fullNameMaritial to "none".
3. Fill in all fields based on the information provided in the Arabic text.
4. If any information is missing, leave the field as an empty string.
''', 'id_back', ai.driverId); //egypt
},
child: Column(
children: [
Image.network(
'${AppLink.server}/card_image/id_back-${ai.driverId}.jpg',
height: Get.height * .25,
width: double.maxFinite,
fit: BoxFit.fitHeight,
),
Text(
'Capture an Image of Your ID Document Back'.tr,
style: AppStyle.title,
),
],
),
),
);
},
);
}
GetBuilder<RegisterCaptainController> egyptDriverIDFront() {
return GetBuilder<RegisterCaptainController>(
builder: (ai) {
if (ai.responseIdEgyptFront.isNotEmpty) {
return Card(
elevation: 4.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.0),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('ID Documents Front'.tr, style: AppStyle.headTitle2),
IconButton(
onPressed: () async {
await ai.allMethodForAI('''
Write a JSON object from the following information extracted from the provided Arabic text:
{
"first_name": "", // The word next to "بطاقة تحقيق الشخصية" (National Identification Card)
"full_name": "", // The full name on the next line after the first name
"address": "", // The complete address spanning the next two lines
"national_number": "", // The National ID number before the last line (convert Arabic numerals to Latin)
"card_id": "", // The card ID in English on the last line
"dob": "" // Year of birth only, in Latin numerals (YYYY format)
}
Important notes:
1. For 'first_name', extract the word immediately following "بطاقة تحقيق الشخصية".
2. 'full_name' should be the complete name found on the line after the first name.
3. 'address' should combine information from two consecutive lines.
4. Convert the 'national_number' from Arabic numerals to Latin numerals (0-9).
5. 'card_id' should be extracted as-is from the last line (it's already in English).
6. For 'dob', include only the year of birth in YYYY format using Latin numerals.
7. If any information is missing, leave the field as an empty string.
''', 'id_front', ai.driverId); //egypt
},
icon: const Icon(Icons.refresh),
),
],
),
const SizedBox(height: 8.0),
const Divider(color: AppColor.accentColor),
const SizedBox(height: 8.0),
// Removed Make, Model, etc. as they are not available
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${'First Name'.tr}: ${ai.responseIdEgyptFront['first_name']}',
),
Text(
'${'CardID'.tr}: ${ai.responseIdEgyptFront['card_id']}',
),
],
),
const SizedBox(height: 8.0),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${'Full Name'.tr}: ${ai.responseIdEgyptFront['full_name']}',
),
Text(
'${'DOB'.tr}: ${ai.responseIdEgyptFront['dob']}',
),
],
),
const SizedBox(height: 8.0),
Text(
'${'Address'.tr}: ${ai.responseIdEgyptFront['address']}',
),
const SizedBox(height: 8.0),
// Text(
// '${'National Number'.tr}: ${ai.responseIdEgyptFront['national_number']}',
// ),
// const SizedBox(height: 8.0),
// Removed Inspection Date as it's not available
],
),
),
);
}
return Card(
child: InkWell(
onTap: () async {
await ai.allMethodForAI(""""
Write a JSON object from the following information extracted from the provided Arabic text:
{
"first_name": "", // The word next to "بطاقة تحقيق الشخصية" (National Identification Card)
"full_name": "", // The full name on the next line after the first name
"address": "", // The complete address spanning the next two lines
"national_number": "", // The National ID number before the last line (convert Arabic numerals to Latin)
"card_id": "", // The card ID in English on the last line
"dob": "" // Year of birth only, in Latin numerals (YYYY format)
}
Important notes:
1. For 'first_name', extract the word immediately following "بطاقة تحقيق الشخصية".
2. 'full_name' should be the complete name found on the line after the first name.
3. 'address' should combine information from two consecutive lines.
4. Convert the 'national_number' from Arabic numerals to Latin numerals (0-9).
5. 'card_id' should be extracted as-is from the last line (it's already in English).
6. For 'dob', include only the year of birth in YYYY format using Latin numerals.
7. If any information is missing, leave the field as an empty string.
""", 'id_front', ai.driverId); //egypt
},
child: Column(
children: [
Image.network(
'${AppLink.server}/card_image/id_front-${ai.driverId}.png',
height: Get.height * .25,
width: double.maxFinite,
fit: BoxFit.fitHeight,
),
Text(
'Capture an Image of Your ID Document front'.tr,
style: AppStyle.title,
),
],
),
),
);
},
);
}
GetBuilder<RegisterCaptainController> egyptCarLicenceFront() {
return GetBuilder<RegisterCaptainController>(
builder: (ai) {
if (ai.responseIdCardDriverEgyptFront.isNotEmpty) {
// No need to access ai.responseIdCardDriverEgyptBack anymore
final licenseExpiryDate = DateTime.parse(
ai.responseIdCardDriverEgyptFront['LicenseExpirationDate']);
// Check if license has expired
final today = DateTime.now();
final isLicenseExpired = licenseExpiryDate.isBefore(today);
return Card(
elevation: 4.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.0),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text('Vehicle Details Front'.tr,
style: AppStyle.headTitle2),
IconButton(
onPressed: () async {
ai.allMethodForAI("""
Extract the following details from the provided car license data and format them into a JSON object:
License Expiration Date
Car Plate
Owner
Address
Car License Data:
JSON Format:
{
"LicenseExpirationDate": "YYYY-MM-DD",
"car_plate": "[Car plate number]",//the car plate is line next to line contain 'ادارة مرور' for bot numbers and letters in arabic with partition like| but you remove |
"owner": "[Owner's full name]",
"address": "[Address if available, otherwise 'Not provided']"
}
Important notes:
1. For the LicenseExpirationDate, ensure the date is in YYYY-MM-DD format using Latin numerals (0-9).
2. Replace all occurrences of '|' (pipe character) with a space in all fields.
3. If any information is missing, leave the corresponding field as an empty string.
4. Ensure all text is properly formatted and spaces are used correctly.
Please fill in the JSON object with the extracted information, following these guidelines.
""", 'car_front', ai.driverId);
},
icon: const Icon(Icons.refresh),
),
],
),
const SizedBox(height: 8.0),
const Divider(color: AppColor.accentColor),
const SizedBox(height: 8.0),
// Removed Make, Model, etc. as they are not available
Text(
'${'Plate Number'.tr}: ${ai.responseIdCardDriverEgyptFront['car_plate']}',
),
const SizedBox(height: 8.0),
Text(
'${'Owner Name'.tr}: ${ai.responseIdCardDriverEgyptFront['owner']}',
),
const SizedBox(height: 8.0),
Text(
'${'Address'.tr}: ${ai.responseIdCardDriverEgyptFront['address']}',
),
const SizedBox(height: 8.0),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${'License Expiry Date'.tr}: ${licenseExpiryDate.toString().substring(0, 10)}',
style: TextStyle(
color: isLicenseExpired ? Colors.red : Colors.green,
),
),
// Removed Fuel as it's not available
],
),
// Removed Inspection Date as it's not available
],
),
),
);
}
return Card(
child: InkWell(
onTap: () async {
ai.allMethodForAI("""
Extract the following details from the provided car license data and format them into a JSON object:
License Expiration Date
Car Plate
Owner
Address
Car License Data:
JSON Format:
{
"LicenseExpirationDate": "YYYY-MM-DD",
"car_plate": "[Car plate number]",//the car plate is line next to line contain 'ادارة مرور' for bot numbers and letters in arabic with partition like| but you remove |
"owner": "[Owner's full name]",
"address": "[Address if available, otherwise 'Not provided']"
}
Important notes:
1. For the LicenseExpirationDate, ensure the date is in YYYY-MM-DD format using Latin numerals (0-9).
2. Replace all occurrences of '|' (pipe character) with a space in all fields.
3. If any information is missing, leave the corresponding field as an empty string.
4. Ensure all text is properly formatted and spaces are used correctly.
Please fill in the JSON object with the extracted information, following these guidelines.
""", 'car_front', ai.driverId);
},
child: Column(
children: [
Image.network(
'${AppLink.server}/card_image/car_front-${ai.driverId}.jpg',
height: Get.height * .25,
width: double.maxFinite,
fit: BoxFit.fitHeight,
),
Text(
'Capture an Image of Your car license front '.tr,
style: AppStyle.title,
),
],
),
),
);
},
);
}
GetBuilder<RegisterCaptainController> egyptCarLicenceBack() {
return GetBuilder<RegisterCaptainController>(
builder: (ai) {
if (ai.responseIdCardDriverEgyptBack.isNotEmpty) {
// Get the tax expiry date from the response
final taxExpiryDate = ai.responseIdCardDriverEgyptBack['tax_expiry'];
// final displacement = ai.responseIdCardDriverEgyptBack['displacement'];
// if (int.parse(displacement) < 1000) {}
// Get the inspection date from the response
final inspectionDate =
ai.responseIdCardDriverEgyptBack['inspection_date'];
final year = int.parse(inspectionDate.split('-')[0]);
// Set inspectionDateTime to December 31st of the given year
final inspectionDateTime = DateTime(year, 12, 31);
String carBackLicenseExpired =
inspectionDateTime.toString().split(' ')[0];
// Get the current date
final today = DateTime.now();
// Try parsing the tax expiry date. If it fails, set it to null.
final taxExpiryDateTime = DateTime.tryParse(taxExpiryDate ?? '');
final isExpired =
taxExpiryDateTime != null && taxExpiryDateTime.isBefore(today);
// Check if the inspection date is before today
bool isInspectionExpired = inspectionDateTime.isBefore(today);
return Card(
elevation: 4.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.0),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Vehicle Details Back'.tr,
style: AppStyle.headTitle2),
IconButton(
onPressed: () async {
ai.allMethodForAI("""
Analyze the extracted car license information and create a JSON object with the following keys:
{
"make": "",
"year": "",
"chassis": "",
"model": "",
"engine": "",
"displacement": "",
"cylinders": "",
"fuel": "",
"color": "",
"color_hex": "",
"inspection_date": "",
"assuranceNumber": "",
"tax_expiry": ""
}
Important notes:
1. For dates (inspection_date and tax_expiry), use the format YYYY-MM-DD with Latin numerals (0-9).
2. Convert the color name to its corresponding hex color code for the 'color_hex' field.
3. Ensure all numeric values (year, displacement, cylinders) are in Latin numerals.
4. If any information is missing, leave the corresponding field as an empty string.
5. Do not include any explanatory text in the JSON fields, only the extracted values.
displacement in the line contain (سم٣ )
Please fill in the JSON object with the extracted information, following these guidelines.
""", 'car_back', ai.driverId);
},
icon: const Icon(Icons.refresh),
),
],
),
const SizedBox(height: 8.0),
const Divider(color: AppColor.accentColor),
const SizedBox(height: 8.0),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${'Make'.tr}: ${ai.responseIdCardDriverEgyptBack['make']}'),
Text(
'${'Model'.tr}: ${ai.responseIdCardDriverEgyptBack['model']}'),
],
),
const SizedBox(height: 8.0),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${'Year'.tr}: ${ai.responseIdCardDriverEgyptBack['year']}'),
Text(
'${'Chassis'.tr}: ${ai.responseIdCardDriverEgyptBack['chassis']}'),
],
),
const SizedBox(height: 8.0),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${'Color'.tr}: ${ai.responseIdCardDriverEgyptBack['color']}'),
Text(
'${'Displacement'.tr}: ${ai.responseIdCardDriverEgyptBack['displacement']} cc'),
],
),
const SizedBox(height: 8.0),
Text(
'${'Fuel'.tr}: ${ai.responseIdCardDriverEgyptBack['fuel']}'),
const SizedBox(height: 8.0),
if (taxExpiryDateTime != null)
Text(
'${'Tax Expiry Date'.tr}: $taxExpiryDate',
style: TextStyle(
color: isExpired ? Colors.red : Colors.green,
),
),
const SizedBox(height: 8.0),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${'Inspection Date'.tr}: $carBackLicenseExpired',
style: TextStyle(
color:
isInspectionExpired ? Colors.red : Colors.green,
),
),
],
),
],
),
),
);
}
return Card(
child: InkWell(
onTap: () async {
ai.allMethodForAI("""
Analyze the extracted car license information and create a JSON object with the following keys:
{
"make": "",
"year": "",
"chassis": "",
"model": "",
"engine": "",
"displacement": "",
"cylinders": "",
"fuel": "",
"color": "",
"color_hex": "",
"inspection_date": "",
"assuranceNumber": "",
"tax_expiry": ""
}
Important notes:
1. For dates (inspection_date and tax_expiry), use the format YYYY-MM-DD with Latin numerals (0-9).
2. Convert the color name to its corresponding hex color code for the 'color_hex' field.
3. Ensure all numeric values (year, displacement, cylinders) are in Latin numerals.
4. If any information is missing, leave the corresponding field as an empty string.
5. Do not include any explanatory text in the JSON fields, only the extracted values.
Please fill in the JSON object with the extracted information, following these guidelines.
""", 'car_back', ai.driverId);
},
child: Column(
children: [
Image.network(
'${AppLink.server}/card_image/car_back-${ai.driverId}.jpg',
height: Get.height * .25,
width: double.maxFinite,
fit: BoxFit.fitHeight,
),
Text(
'Capture an Image of Your car license back'.tr,
style: AppStyle.title,
),
],
),
),
);
},
);
}
GetBuilder<RegisterCaptainController> egyptCriminalRecord() {
return GetBuilder<RegisterCaptainController>(
builder: (ai) {
if (ai.responseCriminalRecordEgypt.isNotEmpty) {
return Card(
elevation: 4.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.0),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Criminal Record'.tr, style: AppStyle.headTitle2),
IconButton(
onPressed: () async {
await ai.allMethodForAI("""
Write a JSON object from the following information extracted from the provided Arabic text:
{
"InspectionResult": "",
"NationalID": "",
"FullName": "",
"IssueDate": "" // Format: YYYY-MM-DD
}
Important notes:
1. For the IssueDate, ensure the date is in YYYY-MM-DD format using Latin numerals (0-9).
2. Add appropriate spaces in all text fields to ensure readability.
3. If any information is missing, leave the corresponding field as an empty string.
4. Ensure all text is properly formatted and spaces are used correctly.
5. Convert any Arabic numerals to Latin numerals (0-9) where applicable.
Please fill in the JSON object with the extracted information, following these guidelines.
""", 'criminalRecord', ai.driverId);
},
icon: const Icon(Icons.refresh),
),
],
),
const SizedBox(height: 8.0),
const Divider(color: AppColor.accentColor),
const SizedBox(height: 8.0),
Text(
'${'InspectionResult'.tr}: ${ai.responseCriminalRecordEgypt['InspectionResult']}'),
const SizedBox(height: 8.0),
Text(
'${'FullName'.tr}: ${ai.responseCriminalRecordEgypt['FullName']}',
style: AppStyle.title.copyWith(
color: ai.responseCriminalRecordEgypt['FullName'] ==
ai.responseIdEgyptDriverLicense['name_arabic']
? AppColor.greenColor
: AppColor.redColor),
),
const SizedBox(height: 8.0),
Text(
'${'NationalID'.tr}: ${ai.responseCriminalRecordEgypt['NationalID']}'),
const SizedBox(height: 8.0),
Text(
'${'IssueDate'.tr}: ${ai.responseCriminalRecordEgypt['IssueDate']}'),
],
),
),
);
}
return Card(
child: InkWell(
onTap: () async {
await ai.allMethodForAI("""
Write a JSON object from the following information extracted from the provided Arabic text:
{
"InspectionResult": "",
"NationalID": "",
"FullName": "",
"IssueDate": "" // Format: YYYY-MM-DD
}
Important notes:
1. For the IssueDate, ensure the date is in YYYY-MM-DD format using Latin numerals (0-9).
2. Add appropriate spaces in all text fields to ensure readability.
3. If any information is missing, leave the corresponding field as an empty string.
4. Ensure all text is properly formatted and spaces are used correctly.
5. Convert any Arabic numerals to Latin numerals (0-9) where applicable.
Please fill in the JSON object with the extracted information, following these guidelines.
""", 'criminalRecord', ai.driverId);
},
child: Column(
children: [
Image.network(
'${AppLink.server}/card_image/6.png',
height: Get.height * .25,
width: double.maxFinite,
fit: BoxFit.fitHeight,
),
Text(
'Capture an Image of Your Criminal Record'.tr,
style: AppStyle.title,
),
],
),
),
);
},
);
}
}

View File

@@ -0,0 +1,54 @@
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 '../../../controller/functions/encrypt_decrypt.dart';
import '../../../main.dart';
import '../../../print.dart';
import 'driver_details_not_active_page.dart';
class DriversPendingPage extends StatelessWidget {
final DriverController controller = Get.put(DriverController());
DriversPendingPage({super.key});
@override
Widget build(BuildContext context) {
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()));
},
);
},
);
},
),
);
}
}

View File

@@ -0,0 +1,287 @@
import 'package:flutter/material.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';
class ComplaintListPage extends StatelessWidget {
ComplaintListPage({super.key});
final ComplaintController controller = Get.put(ComplaintController());
@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),
),
),
],
),
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);
},
),
);
}),
],
);
}
Widget _buildComplaintCard(BuildContext context, dynamic c) {
Color statusColor = _getStatusColor(c['statusComplaint']);
return Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: AppStyle.cardDecoration,
child: ExpansionTile(
tilePadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
leading: _buildStatusIndicator(c['statusComplaint'], statusColor),
title: Text(
c['description']?.toString() ?? 'بدون وصف',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: AppStyle.title,
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
Text(
'النوع: ${c['complaint_type'] ?? 'عام'} | الرحلة: ${c['ride_id']}',
style: AppStyle.caption,
),
const SizedBox(height: 4),
Row(
children: [
Icon(Icons.person_rounded, size: 12, color: AppColor.textSecondary),
const SizedBox(width: 4),
Text(c['passengerName'] ?? 'غير معروف', style: AppStyle.caption),
const SizedBox(width: 12),
Icon(Icons.drive_eta_rounded, size: 12, color: AppColor.textSecondary),
const SizedBox(width: 4),
Text(c['driverName'] ?? 'غير معروف', style: AppStyle.caption),
],
),
],
),
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Divider(color: AppColor.divider),
_buildInfoRow('الوصف', c['description'] ?? 'لا يوجد وصف'),
const SizedBox(height: 12),
_buildInfoRow('الحل الحالي', c['resolution'] ?? 'لم يتم الحل بعد'),
const SizedBox(height: 12),
_buildRideDetails(c),
const SizedBox(height: 24),
Row(
children: [
Expanded(
child: MyElevatedButton(
title: 'تحديث الحالة / حل الشكوى',
onPressed: () => _showResolveDialog(context, c),
kolor: AppColor.accent,
),
),
],
),
],
),
),
],
),
);
}
Widget _buildStatusIndicator(String? status, Color color) {
return Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: color.withOpacity(0.12),
shape: BoxShape.circle,
border: Border.all(color: color.withOpacity(0.25)),
),
child: Icon(
status == 'Resolved' ? Icons.check_circle_rounded : Icons.pending_rounded,
color: color,
size: 20,
),
);
}
Widget _buildInfoRow(String label, String value) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: AppStyle.caption.copyWith(color: AppColor.accent)),
const SizedBox(height: 4),
Text(value, style: AppStyle.body),
],
);
}
Widget _buildRideDetails(dynamic c) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColor.surfaceElevated,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColor.divider),
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildSmallStat('السعر', '${c['priceOfRide']} ل.س'),
_buildSmallStat('التقييم', '${c['avgRatingDriverFromPassengers'] ?? 0}'),
_buildSmallStat('النوع', c['ascarType'] ?? 'N/A'),
],
),
],
),
);
}
Widget _buildSmallStat(String label, String value) {
return Column(
children: [
Text(label, style: AppStyle.caption.copyWith(fontSize: 10)),
const SizedBox(height: 2),
Text(value, style: AppStyle.number.copyWith(fontSize: 12)),
],
);
}
Color _getStatusColor(String? status) {
switch (status) {
case 'Open': return AppColor.danger;
case 'In Progress': return AppColor.warning;
case 'Resolved': return AppColor.success;
default: return AppColor.textSecondary;
}
}
void _showResolveDialog(BuildContext context, dynamic c) {
final TextEditingController resController = TextEditingController(text: c['resolution']);
String selectedStatus = c['statusComplaint'] ?? 'Open';
Get.bottomSheet(
StatefulBuilder(
builder: (context, setModalState) => Container(
padding: const EdgeInsets.all(24),
decoration: const BoxDecoration(
color: AppColor.surfaceElevated,
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('تحديث حالة الشكوى', style: AppStyle.headTitle),
const SizedBox(height: 20),
_buildStatusDropdown(selectedStatus, (val) {
setModalState(() => selectedStatus = val!);
}),
const SizedBox(height: 20),
MyTextForm(
controller: resController,
label: 'قرار الحل / الملاحظات',
hint: 'اكتب تفاصيل الحل هنا...',
type: TextInputType.multiline,
prefixIcon: Icons.gavel_rounded,
),
const SizedBox(height: 24),
MyElevatedButton(
title: 'حفظ التحديث',
onPressed: () async {
bool success = await controller.updateComplaintStatus(
c['id'].toString(),
selectedStatus,
resController.text
);
if (success) Get.back();
},
),
const SizedBox(height: 20),
],
),
),
),
isScrollControlled: true,
);
}
Widget _buildStatusDropdown(String current, Function(String?) onChanged) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColor.divider),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: current,
isExpanded: true,
dropdownColor: AppColor.surfaceElevated,
items: ['Open', 'In Progress', 'Resolved']
.map((s) => DropdownMenuItem(value: s, child: Text(s.tr, style: AppStyle.body)))
.toList(),
onChanged: onChanged,
),
),
);
}
}

View File

@@ -0,0 +1,339 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_admin/constant/colors.dart';
import 'package:siro_admin/controller/admin/dashboard_v2_controller.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
class DashboardV2Widget extends StatelessWidget {
const DashboardV2Widget({super.key});
@override
Widget build(BuildContext context) {
// Initialize controller
final controller = Get.put(DashboardV2Controller());
return GetBuilder<DashboardV2Controller>(
builder: (ctrl) {
if (ctrl.isLoading) {
return const SliverToBoxAdapter(
child: SizedBox(
height: 150,
child: Center(
child: CircularProgressIndicator(color: AppColor.accent),
),
),
);
}
return SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 1. Real-time stats
_buildSectionTitle('مركز العمليات الحي (Real-time)'),
_buildRealtimeStats(ctrl.realtimeData),
const SizedBox(height: 20),
// 2. Smart Alerts
if (ctrl.smartAlerts.isNotEmpty) ...[
_buildSectionTitle('التنبيهات الذكية (${ctrl.smartAlerts.length})'),
_buildSmartAlerts(ctrl.smartAlerts),
const SizedBox(height: 10),
]
],
),
);
},
);
}
Widget _buildSectionTitle(String title) {
return Padding(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 10),
child: Row(
children: [
Container(
width: 3,
height: 14,
decoration: BoxDecoration(
color: AppColor.danger, // Distinct color
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 8),
Text(
title,
style: const TextStyle(
color: AppColor.textSecondary,
fontSize: 12,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
),
],
),
);
}
Widget _buildRealtimeStats(Map<String, dynamic> data) {
final stats = [
{
'title': 'رحلات نشطة',
'value': data['active_rides']?.toString() ?? '0',
'icon': Icons.directions_car_rounded,
'color': AppColor.info,
},
{
'title': 'سائقون أونلاين',
'value': data['online_drivers']?.toString() ?? '0',
'icon': Icons.wifi_tethering,
'color': AppColor.success,
},
{
'title': 'إيرادات اليوم',
'value': '${data['revenue_today'] ?? 0}',
'icon': Icons.monetization_on_rounded,
'color': AppColor.warning,
},
{
'title': 'إيرادات الأمس',
'value': '${data['revenue_yesterday'] ?? 0}',
'icon': Icons.history_rounded,
'color': Colors.grey,
},
{
'title': 'شكاوى مفتوحة',
'value': data['new_complaints']?.toString() ?? '0',
'icon': Icons.warning_rounded,
'color': AppColor.danger,
},
{
'title': 'رخص تنتهي قريبًا',
'value': data['expiring_licenses']?.toString() ?? '0',
'icon': Icons.sd_card_alert_rounded,
'color': Colors.orangeAccent,
},
];
return SizedBox(
height: 110,
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
itemCount: stats.length,
itemBuilder: (ctx, i) {
final stat = stats[i];
return Padding(
padding: const EdgeInsets.only(right: 10),
child: _buildStatCard(stat),
);
},
),
);
}
Widget _buildStatCard(Map<String, dynamic> stat) {
final color = stat['color'] as Color;
return Container(
width: 140,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: color.withOpacity(0.3)),
boxShadow: [
BoxShadow(
color: color.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: color.withOpacity(0.15),
borderRadius: BorderRadius.circular(8),
),
child: Icon(stat['icon'] as IconData, color: color, size: 16),
),
const Spacer(),
// Pulsing indicator for active things
if (stat['title'] == 'رحلات نشطة' || stat['title'] == 'سائقون أونلاين')
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: color,
boxShadow: [
BoxShadow(
color: color.withOpacity(0.5),
blurRadius: 4,
spreadRadius: 1,
)
]
),
),
],
),
const Spacer(),
Text(
stat['value'].toString(),
style: const TextStyle(
color: AppColor.textPrimary,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 2),
Text(
stat['title'].toString(),
style: const TextStyle(
color: AppColor.textSecondary,
fontSize: 10,
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
);
}
Widget _buildSmartAlerts(List<dynamic> alerts) {
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: alerts.length > 5 ? 5 : alerts.length, // Show top 5
itemBuilder: (context, index) {
final alert = alerts[index];
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 300),
child: SlideAnimation(
verticalOffset: 20.0,
child: FadeInAnimation(
child: _buildAlertItem(alert),
),
),
);
},
);
}
Widget _buildAlertItem(Map<String, dynamic> alert) {
Color getSeverityColor(String severity) {
switch (severity) {
case 'high':
return AppColor.danger;
case 'medium':
return AppColor.warning;
case 'warning':
return Colors.orangeAccent;
default:
return AppColor.info;
}
}
IconData getAlertIcon(String type) {
switch (type) {
case 'complaint':
return Icons.report_problem_rounded;
case 'ride':
return Icons.directions_car_rounded;
case 'license':
return Icons.badge_rounded;
default:
return Icons.notifications_active_rounded;
}
}
final color = getSeverityColor(alert['severity']);
final icon = getAlertIcon(alert['type']);
return Container(
margin: const EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withOpacity(0.4)),
boxShadow: [
BoxShadow(
color: color.withOpacity(0.05),
blurRadius: 5,
offset: const Offset(0, 2),
)
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, color: color, size: 20),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
alert['title'] ?? '',
style: const TextStyle(
color: AppColor.textPrimary,
fontSize: 13,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
alert['description'] ?? '',
style: const TextStyle(
color: AppColor.textSecondary,
fontSize: 11,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
const SizedBox(width: 8),
Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
alert['date'] != null
? alert['date'].toString().split(' ')[0]
: '',
style: const TextStyle(
color: AppColor.textSecondary,
fontSize: 10,
),
),
const SizedBox(height: 10),
Icon(Icons.arrow_forward_ios_rounded,
color: AppColor.textSecondary, size: 12),
],
),
],
),
);
}
}

View File

@@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart'; // For Get.width if needed, and .tr
import 'package:siro_admin/constant/colors.dart'; // Assuming AppColor is here
import 'package:siro_admin/constant/style.dart'; // Assuming AppStyle is here
class DashboardStatCard extends StatelessWidget {
final String title;
final String value;
final IconData? icon;
final Color? iconColor;
final Color? backgroundColor;
final Color? valueColor;
const DashboardStatCard({
Key? key,
required this.title,
required this.value,
this.icon,
this.iconColor,
this.backgroundColor,
this.valueColor,
}) : super(key: key);
@override
Widget build(BuildContext context) {
// Attempt to use AppStyle.boxDecoration1 properties if it's a BoxDecoration
BoxDecoration? baseDecoration = AppStyle.boxDecoration1;
Color? finalBackgroundColor =
backgroundColor ?? baseDecoration?.color ?? Theme.of(context).cardColor;
BorderRadius? finalBorderRadius =
baseDecoration?.borderRadius?.resolve(Directionality.of(context)) ??
BorderRadius.circular(12.0);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 14.0, vertical: 12.0),
decoration: BoxDecoration(
color: finalBackgroundColor,
borderRadius: finalBorderRadius,
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
spreadRadius: 1,
blurRadius: 6,
offset: const Offset(0, 2),
),
],
// If AppStyle.boxDecoration1 includes a border, you might want to add it here too
// border: baseDecoration?.border,
),
child: Column(
mainAxisAlignment:
MainAxisAlignment.center, // Center content vertically
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
child: Text(
title.tr,
style: AppStyle.title.copyWith(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodySmall?.color,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
if (icon != null)
Icon(
icon,
size: 24,
color: iconColor ?? AppColor.primaryColor.withOpacity(0.7),
),
],
),
const SizedBox(height: 6),
Text(
value,
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: valueColor ?? AppColor.primaryColor,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
);
}
}

View File

@@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_admin/constant/links.dart';
import 'package:siro_admin/controller/functions/crud.dart';
import 'package:siro_admin/controller/functions/wallet.dart';
import 'package:siro_admin/views/widgets/my_scafold.dart';
import '../../../controller/drivers/driverthebest.dart';
class DriverTheBestAlexandria extends StatelessWidget {
const DriverTheBestAlexandria({super.key});
@override
Widget build(BuildContext context) {
Get.put(DriverTheBestAlexandriaController(), permanent: true);
return MyScafolld(
title: 'Alexandria'.tr,
body: [
GetBuilder<DriverTheBestAlexandriaController>(builder: (driverthebest) {
return driverthebest.driver.isNotEmpty
? ListView.builder(
itemCount: driverthebest.driver.length,
itemBuilder: (context, index) {
final driver = driverthebest.driver[index];
return ListTile(
leading: CircleAvatar(
child: Text(
((driver['driver_count'] * 5) / 3600)
.toStringAsFixed(0),
),
),
title: Text(driver['name_arabic'] ?? 'Unknown Name'),
subtitle: Text('Phone: ${driver['phone'] ?? 'N/A'}'),
trailing: IconButton(
onPressed: () async {
Get.defaultDialog(
title:
'are you sure to pay to this driver gift'.tr,
middleText: '',
onConfirm: () async {
final wallet = Get.put(WalletController());
await wallet.addPaymentToDriver('100',
driver['id'].toString(), driver['token']);
await wallet.addSeferWallet(
'100', driver['id'].toString());
await CRUD().post(
link: AppLink.deleteRecord,
payload: {
'driver_id': driver['id'].toString()
});
driverthebest.driver.removeAt(index);
driverthebest.update();
},
onCancel: () => Get.back());
},
icon: const Icon(Icons.wallet_giftcard_rounded),
),
);
},
)
: const Center(
child: Text('No drivers available.'),
);
})
],
isleading: true,
);
}
}

View File

@@ -0,0 +1,218 @@
import 'package:flutter/material.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 '../../../constant/links.dart';
class DriverDocsReviewPage extends StatelessWidget {
DriverDocsReviewPage({super.key});
final DriverDocsController controller = Get.put(DriverDocsController());
@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);
},
),
),
)),
],
);
}
Widget _buildDriverCard(BuildContext context, dynamic driver) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
decoration: AppStyle.cardDecoration,
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)),
),
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),
onTap: () => _showDriverDetails(context, driver['id'].toString()),
),
);
}
void _showDriverDetails(BuildContext context, String id) async {
final details = await controller.getDriverFullDetails(id);
if (details == null) return;
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)).toList(),
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();
Get.snackbar('نجاح', 'تم تفعيل حساب السائق بنجاح');
}
},
),
const SizedBox(height: 100),
],
),
),
],
));
}
Widget _buildDriverHeader(dynamic driver) {
return Container(
padding: const EdgeInsets.all(20),
decoration: AppStyle.elevatedCard,
child: Column(
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${driver['first_name']} ${driver['last_name']}', style: AppStyle.headTitle),
Text(driver['phone'] ?? '', style: AppStyle.subtitle),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: AppColor.warning.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: Text('Pending', style: AppStyle.caption.copyWith(color: AppColor.warning)),
),
],
),
const Divider(height: 32, color: AppColor.divider),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildSmallInfo('الرقم الوطني', driver['national_number'] ?? 'N/A'),
_buildSmallInfo('الجنس', driver['gender'] ?? 'N/A'),
_buildSmallInfo('تاريخ الميلاد', driver['birthdate'] ?? 'N/A'),
],
),
],
),
);
}
Widget _buildSmallInfo(String label, String value) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: AppStyle.caption.copyWith(fontSize: 10)),
const SizedBox(height: 2),
Text(value, style: AppStyle.body.copyWith(fontSize: 12, fontWeight: FontWeight.bold)),
],
);
}
Widget _buildDocCard(dynamic doc) {
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,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
const Icon(Icons.file_present_rounded, color: AppColor.accent, size: 20),
const SizedBox(width: 8),
Text(doc['doc_type'] ?? 'وثيقة', style: AppStyle.title.copyWith(fontSize: 14)),
],
),
),
ClipRRect(
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(16)),
child: Image.network(
imageUrl,
width: double.infinity,
height: 200,
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)),
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,230 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_admin/controller/functions/crud.dart';
import 'package:siro_admin/controller/functions/wallet.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 = [];
@override
void onInit() {
super.onInit();
// fetchDriverCache(); // تحميل البيانات عند فتح الصفحة
}
// 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']);
}
} catch (e) {
debugPrint("Error loading cache: $e");
}
}
// --- الدالة الرئيسية التي تنفذ العملية المطلوبة ---
Future<void> processDriverGift() async {
String phoneInput = phoneController.text.trim();
if (phoneInput.isEmpty) {
Get.snackbar("تنبيه", "يرجى إدخال رقم الهاتف",
backgroundColor: Colors.orange);
return;
}
await fetchDriverCache();
isLoading.value = true;
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);
}
return dbPhone == inputPhone;
},
orElse: () => null,
);
if (driver == null) {
statusLog.value = "❌ لم يتم العثور على سائق بهذا الرقم في الكاش.";
isLoading.value = false;
return;
}
String driverId = driver['id'].toString();
String driverName = driver['name_arabic'] ?? 'بدون اسم';
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 {
isLoading.value = false;
}
}
// دالة إضافة الهدية باستخدام 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());
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)
],
),
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)),
),
)),
],
),
),
const SizedBox(height: 30),
// منطقة عرض النتائج (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),
)),
),
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,654 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart'; // Ensure get_storage is in pubspec.yaml
import 'package:siro_admin/controller/functions/wallet.dart';
// --- New Controller to handle the specific JSON URL ---
class DriverCacheController extends GetxController {
List<dynamic> drivers = [];
bool isLoading = false;
String lastUpdated = '';
String searchQuery = ''; // Search query state
// Storage for paid drivers
final box = GetStorage();
List<String> paidDrivers = [];
@override
void onInit() {
super.onInit();
// Load previously paid drivers from storage
var stored = box.read('paid_drivers');
if (stored != null) {
paidDrivers = List<String>.from(stored.map((e) => e.toString()));
}
fetchData();
}
Future<void> fetchData() async {
isLoading = true;
update(); // Notify UI to show loader
try {
// Using GetConnect to fetch the JSON directly
final response = await GetConnect().get(
'https://api.intaleq.xyz/intaleq/ride/location/active_drivers_cache.json',
);
if (response.body != null && response.body is Map) {
if (response.body['data'] != null) {
drivers = List<dynamic>.from(response.body['data']);
}
if (response.body['last_updated'] != null) {
lastUpdated = response.body['last_updated'].toString();
}
}
} catch (e) {
debugPrint("Error fetching driver cache: $e");
} finally {
isLoading = false;
update(); // Update UI with data
}
}
// Update search query
void updateSearchQuery(String query) {
searchQuery = query;
update();
}
// Mark driver as paid and save to storage
void markAsPaid(String driverId) {
// Validation: Don't mark if ID is invalid
if (driverId == 'null' || driverId.isEmpty) return;
if (!paidDrivers.contains(driverId)) {
paidDrivers.add(driverId);
box.write('paid_drivers', paidDrivers);
update();
}
}
// Clear all paid status (Delete Box)
void clearPaidStorage() {
paidDrivers.clear();
box.remove('paid_drivers');
update();
Get.snackbar(
"Storage Cleared",
"Paid status history has been reset",
backgroundColor: Colors.redAccent,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
);
}
// Check if driver is already paid
bool isDriverPaid(String driverId) {
return paidDrivers.contains(driverId);
}
}
class DriverTheBestRedesigned extends StatelessWidget {
const DriverTheBestRedesigned({super.key});
@override
Widget build(BuildContext context) {
// Put the new controller
final controller = Get.put(DriverCacheController());
return Scaffold(
backgroundColor: const Color(0xFFF8FAFC), // slate-50 background
body: SafeArea(
child: GetBuilder<DriverCacheController>(builder: (ctrl) {
if (ctrl.isLoading) {
return const Center(child: CircularProgressIndicator());
}
// Filter List based on Search Query
List<dynamic> filteredDrivers = ctrl.drivers.where((driver) {
if (ctrl.searchQuery.isEmpty) return true;
final phone = driver['phone']?.toString() ?? '';
// Simple contains check for phone
return phone.contains(ctrl.searchQuery);
}).toList();
// Sort by Active Time (Hours) Descending
// We use the filtered list for sorting and display
filteredDrivers.sort((a, b) {
double hoursA = _calculateHoursFromStr(a['active_time']);
double hoursB = _calculateHoursFromStr(b['active_time']);
return hoursB.compareTo(hoursA);
});
// --- 1. Calculate Stats (Based on ALL drivers, not just filtered, to keep dashboard stable) ---
int totalDrivers = ctrl.drivers.length;
int eliteCount = 0;
int inactiveCount = 0;
double maxTime = 0.0;
for (var driver in ctrl.drivers) {
double hours = _calculateHoursFromStr(driver['active_time']);
if (hours > maxTime) maxTime = hours;
if (hours >= 50) {
eliteCount++;
} else if (hours < 5) {
inactiveCount++;
}
}
return Column(
children: [
// --- 2. Header (Slate-900 style) ---
Container(
padding: const EdgeInsets.all(16),
decoration: const BoxDecoration(
color: Color(0xFF0F172A), // slate-900
boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 4)],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.local_taxi,
color: Colors.yellow, size: 24),
const SizedBox(width: 8),
Text(
'Best Drivers Dashboard'.tr,
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 4),
Row(
children: [
const Icon(Icons.access_time,
color: Colors.grey, size: 12),
const SizedBox(width: 4),
Text(
ctrl.lastUpdated.isNotEmpty
? 'Updated: ${ctrl.lastUpdated}'
: 'Data Live',
style: TextStyle(
color: Colors.grey[400], fontSize: 12),
),
],
),
],
),
// Action Buttons (Delete Box & Refresh)
Row(
children: [
// Delete Box Icon
IconButton(
onPressed: () {
Get.defaultDialog(
title: "Reset Paid Status",
middleText:
"Are you sure you want to clear the list of paid drivers? This cannot be undone.",
textConfirm: "Yes, Clear",
textCancel: "Cancel",
confirmTextColor: Colors.white,
buttonColor: Colors.red,
onConfirm: () {
ctrl.clearPaidStorage();
Get.back();
},
);
},
icon: const Icon(Icons.delete_forever,
color: Colors.redAccent),
tooltip: "Clear Paid Storage",
style: IconButton.styleFrom(
backgroundColor: Colors.white10),
),
const SizedBox(width: 8),
IconButton(
onPressed: () {
ctrl.fetchData();
},
icon: const Icon(Icons.refresh,
color: Colors.blueAccent),
style: IconButton.styleFrom(
backgroundColor: Colors.white10),
),
],
)
],
),
),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// --- 3. Statistics Cards Grid ---
SizedBox(
height: 100, // Fixed height for cards
child: Row(
children: [
Expanded(
child: _buildStatCard('Total',
totalDrivers.toString(), Colors.blue)),
const SizedBox(width: 8),
Expanded(
child: _buildStatCard('Elite',
eliteCount.toString(), Colors.amber)),
],
),
),
const SizedBox(height: 8),
SizedBox(
height: 100,
child: Row(
children: [
Expanded(
child: _buildStatCard('Inactive',
inactiveCount.toString(), Colors.red)),
const SizedBox(width: 8),
Expanded(
child: _buildStatCard(
'Max Time',
'${maxTime.toStringAsFixed(1)}h',
Colors.green)),
],
),
),
const SizedBox(height: 24),
// --- 4. Search Bar ---
TextField(
onChanged: (val) => ctrl.updateSearchQuery(val),
decoration: InputDecoration(
hintText: 'Search by phone number...',
prefixIcon:
const Icon(Icons.search, color: Colors.grey),
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade200),
),
),
),
const SizedBox(height: 16),
// --- 5. Driver List ---
if (filteredDrivers.isEmpty)
Center(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Text(
ctrl.searchQuery.isNotEmpty
? "No drivers found with this number"
: "No drivers available",
style: TextStyle(color: Colors.grey[400])),
))
else
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: filteredDrivers.length,
separatorBuilder: (c, i) =>
const SizedBox(height: 12),
itemBuilder: (context, index) {
final driver = filteredDrivers[index];
return _buildDriverCard(
context, driver, index, ctrl);
},
),
],
),
),
),
],
);
}),
),
);
}
// --- Helper Methods ---
// Updated to parse the Arabic string format "5 ساعة 30 دقيقة"
double _calculateHoursFromStr(dynamic activeTimeStr) {
if (activeTimeStr == null || activeTimeStr is! String) return 0.0;
try {
int hours = 0;
int mins = 0;
// Extract hours
final hoursMatch = RegExp(r'(\d+)\s*ساعة').firstMatch(activeTimeStr);
if (hoursMatch != null) {
hours = int.parse(hoursMatch.group(1) ?? '0');
}
// Extract minutes
final minsMatch = RegExp(r'(\d+)\s*دقيقة').firstMatch(activeTimeStr);
if (minsMatch != null) {
mins = int.parse(minsMatch.group(1) ?? '0');
}
return hours + (mins / 60.0);
} catch (e) {
return 0.0;
}
}
Widget _buildStatCard(String title, String value, Color color) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border(right: BorderSide(color: color, width: 4)),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2))
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(title,
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Text(value,
style: TextStyle(
fontSize: 22,
color: color.withOpacity(0.8),
fontWeight: FontWeight.bold)),
],
),
);
}
Widget _buildDriverCard(BuildContext context, Map driver, int index,
DriverCacheController controller) {
double hours = _calculateHoursFromStr(driver['active_time']);
String driverId = driver['id']?.toString() ?? 'null';
bool isPaid = controller.isDriverPaid(driverId);
// Determine Status Category (mimicking HTML logic)
String statusText;
Color statusColor;
if (hours >= 50) {
statusText = "Elite";
statusColor = Colors.amber;
} else if (hours >= 20) {
statusText = "Stable";
statusColor = Colors.green;
} else if (hours >= 5) {
statusText = "Experimental";
statusColor = Colors.blue;
} else {
statusText = "Inactive";
statusColor = Colors.red;
}
// Override colors if paid
Color cardBackground = isPaid ? Colors.teal.shade50 : Colors.white;
Color borderColor = isPaid ? Colors.teal : Colors.transparent;
// Calculate progress (max assumed 60 hours for 100% bar)
double progress = (hours / 60).clamp(0.0, 1.0);
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: cardBackground,
border: isPaid ? Border.all(color: borderColor, width: 2) : null,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4))
],
),
child: Column(
children: [
if (isPaid)
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: Colors.teal,
borderRadius: BorderRadius.circular(4)),
child: const Text("PAID",
style: TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold)),
)
],
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Avatar
CircleAvatar(
backgroundColor: statusColor.withOpacity(0.1),
radius: 24,
child: Text(
hours.toStringAsFixed(0),
style: TextStyle(
color: statusColor, fontWeight: FontWeight.bold),
),
),
const SizedBox(width: 12),
// Name and Phone
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
driver['name_arabic'] ?? 'Unknown Name',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: isPaid
? Colors.teal.shade900
: const Color(0xFF334155)),
),
const SizedBox(height: 4),
Text(
driver['phone'] ?? 'N/A',
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 12,
color: Colors.grey),
),
const SizedBox(height: 2),
Text(
driver['active_time'] ?? '',
style: TextStyle(fontSize: 10, color: Colors.grey[400]),
),
],
),
),
// Status Badge
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: statusColor.withOpacity(0.2)),
),
child: Text(
statusText,
style: TextStyle(
fontSize: 10,
color: statusColor,
fontWeight: FontWeight.bold),
),
),
],
),
const SizedBox(height: 12),
// Progress Bar
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("Performance",
style: TextStyle(fontSize: 10, color: Colors.grey[600])),
Text("${hours.toStringAsFixed(2)} hrs",
style: const TextStyle(
fontSize: 10, fontWeight: FontWeight.bold)),
],
),
const SizedBox(height: 4),
LinearProgressIndicator(
value: progress,
backgroundColor: Colors.grey[100],
color: statusColor,
minHeight: 6,
borderRadius: BorderRadius.circular(3),
),
],
),
const SizedBox(height: 16),
const Divider(height: 1),
const SizedBox(height: 8),
// Actions Row
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
// Pay Gift Button (The specific request)
isPaid
? const Text("Payment Completed",
style: TextStyle(
color: Colors.teal, fontWeight: FontWeight.bold))
: ElevatedButton.icon(
onPressed: () {
_showPayDialog(driver, controller);
},
icon: const Icon(Icons.card_giftcard, size: 16),
label: Text("Pay Gift".tr),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.indigo, // Dark blue/purple
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8)),
),
),
],
)
],
),
);
}
void _showPayDialog(Map driver, DriverCacheController controller) {
// Check for valid ID immediately
String driverId = driver['driver_id']?.toString() ?? '';
String phone = driver['phone']?.toString() ?? '';
if (driverId.isEmpty || driverId == 'null') {
Get.snackbar("Error", "Cannot pay driver with missing ID",
backgroundColor: Colors.red, colorText: Colors.white);
return;
}
// Controller for the Amount Field
final TextEditingController amountController =
TextEditingController(text: '50000');
Get.defaultDialog(
title: 'Confirm Payment',
titleStyle: const TextStyle(
color: Color(0xFF0F172A), fontWeight: FontWeight.bold),
content: Column(
children: [
const Icon(Icons.wallet_giftcard, size: 50, color: Colors.indigo),
const SizedBox(height: 10),
Text(
'Sending gift to ${driver['name_arabic']}',
textAlign: TextAlign.center,
),
const SizedBox(height: 15),
// Amount Field
TextFormField(
controller: amountController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'Amount (SYP)',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.attach_money),
),
),
],
),
textConfirm: 'Pay Now',
confirmTextColor: Colors.white,
buttonColor: Colors.indigo,
onConfirm: () async {
final wallet = Get.put(WalletController());
// Get amount from field
String amount = amountController.text.trim();
if (amount.isEmpty) amount = '0';
// String driverToken = driver['token'] ?? '';
// 1. Add Payment
await wallet.addDriverWallet('gift_connect', driverId, amount, phone);
// 2. Add to Sefer Wallet
//await wallet.addSeferWallet(amount, driverId);
// 3. Delete Record via CRUD
// await CRUD()
// .post(link: AppLink.deleteRecord, payload: {'driver_id': driverId});
// 4. UI Update & Storage
// Mark as paid instead of removing completely, so we can see the color change
controller.markAsPaid(driverId);
Get.back(); // Close Dialog
Get.snackbar("Success", "Payment of $amount EGP sent to driver",
backgroundColor: Colors.green,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM);
},
textCancel: 'Cancel',
onCancel: () => Get.back(),
);
}
}

View File

@@ -0,0 +1,837 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:http/http.dart' as http;
import 'package:siro_admin/constant/links.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../constant/box_name.dart';
import '../../../controller/functions/crud.dart';
import '../../../main.dart';
class IntaleqTrackerScreen extends StatefulWidget {
const IntaleqTrackerScreen({super.key});
@override
State<IntaleqTrackerScreen> createState() => _IntaleqTrackerScreenState();
}
class _IntaleqTrackerScreenState extends State<IntaleqTrackerScreen>
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
void initState() {
super.initState();
_initAnimations();
fetchData();
_timer = Timer.periodic(const Duration(minutes: 5), (timer) {
if (mounted) fetchData();
});
}
void _initAnimations() {
_fadeController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_scaleController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_fadeController.forward();
_scaleController.forward();
}
@override
void dispose() {
_timer?.cancel();
_mapController.dispose();
_fadeController.dispose();
_scaleController.dispose();
super.dispose();
}
Future<void> _makePhoneCall(String phoneNumber) async {
final Uri launchUri = Uri(scheme: 'tel', path: phoneNumber);
if (await canLaunchUrl(launchUri)) {
await launchUrl(launchUri);
} else {
_showSnackBar("لا يمكن إجراء الاتصال لهذا الرقم");
}
}
void _showSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: const Color(0xFF2C3E50),
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.all(16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
);
}
Future<void> fetchData() async {
if (!mounted) return;
setState(() => isLoading = true);
try {
String updateUrl =
"${_baseDir}getUpdatedLocationForAdmin.php?mode=${isLiveMode ? 'live' : 'day'}";
print("📡 Calling Update URL: $updateUrl");
var responseUpdate = await CRUD().post(link: updateUrl, payload: {});
print("📡 Update Response: $responseUpdate");
String v = DateTime.now().millisecondsSinceEpoch.toString();
String liveUrl = "${_baseDir}locations_live.json?v=$v";
print("📡 Calling Live JSON URL: $liveUrl");
final responseLive = await http.get(Uri.parse(liveUrl));
print(
"📡 Live JSON Response (${responseLive.statusCode}): ${responseLive.body.length > 100 ? responseLive.body.substring(0, 100) : responseLive.body}");
if (responseLive.statusCode == 200) {
final data = json.decode(responseLive.body);
List drivers = (data is Map && data.containsKey('drivers'))
? data['drivers']
: data;
setState(() {
liveCount = drivers.length;
if (isLiveMode) _buildMarkers(drivers);
});
}
String dayUrl = "${_baseDir}locations_day.json?v=$v";
print("📡 Calling Day JSON URL: $dayUrl");
final responseDay = await http.get(Uri.parse(dayUrl));
print(
"📡 Day JSON Response (${responseDay.statusCode}): ${responseDay.body.length > 100 ? responseDay.body.substring(0, 100) : responseDay.body}");
if (responseDay.statusCode == 200) {
final data = json.decode(responseDay.body);
List drivers = (data is Map && data.containsKey('drivers'))
? data['drivers']
: data;
setState(() {
dayCount = drivers.length;
if (!isLiveMode) _buildMarkers(drivers);
});
}
setState(() {
lastUpdated = DateTime.now().toString().substring(11, 19);
});
} catch (e) {
print("Exception: $e");
setState(() => lastUpdated = "خطأ في الاتصال");
} finally {
if (mounted) setState(() => isLoading = false);
}
}
void _buildMarkers(List<dynamic> drivers) {
List<Marker> newMarkers = [];
for (var d in drivers) {
double lat = double.tryParse((d['lat'] ?? "0").toString()) ?? 0.0;
double lon = double.tryParse((d['lon'] ?? "0").toString()) ?? 0.0;
double heading = double.tryParse((d['heading'] ?? "0").toString()) ?? 0.0;
String id = (d['id'] ?? "Unknown").toString();
String speed = (d['speed'] ?? "0").toString();
String name = (d['name'] ?? "كابتن").toString();
String phone = (d['phone'] ?? "").toString();
String completed = (d['completed'] ?? "0").toString();
String cancelled = (d['cancelled'] ?? "0").toString();
if (lat != 0 && lon != 0) {
newMarkers.add(
Marker(
point: LatLng(lat, lon),
width: 60,
height: 60,
child: GestureDetector(
onTap: () {
_showDriverInfoDialog(
driverId: id,
name: name,
phone: phone,
speed: speed,
heading: heading,
completed: completed,
cancelled: cancelled,
);
},
child: _buildMarkerWidget(heading),
),
),
);
}
}
setState(() {
_markers = newMarkers;
});
}
Widget _buildMarkerWidget(double heading) {
return Stack(
alignment: Alignment.center,
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: isLiveMode
? [const Color(0xFF27AE60), const Color(0xFF229954)]
: [const Color(0xFF3498DB), const Color(0xFF2980B9)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(
color: (isLiveMode
? const Color(0xFF27AE60)
: const Color(0xFF3498DB))
.withOpacity(0.5),
blurRadius: 12,
spreadRadius: 2,
)
],
),
),
Transform.rotate(
angle: heading * (math.pi / 180),
child: Icon(
Icons.navigation,
color: Colors.white,
size: 26,
),
),
],
);
}
void _showDriverInfoDialog({
required String driverId,
required String name,
required String phone,
required String speed,
required double heading,
required String completed,
required String cancelled,
}) {
showDialog(
context: context,
builder: (_) => Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
backgroundColor: Colors.transparent,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.white, Colors.grey.shade50],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
blurRadius: 30,
spreadRadius: 5,
)
],
),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: isLiveMode
? [const Color(0xFF27AE60), const Color(0xFF229954)]
: [const Color(0xFF3498DB), const Color(0xFF2980B9)],
),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.person_outline, color: Colors.white, size: 20),
const SizedBox(width: 8),
const Text(
"معلومات الكابتن",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
),
const SizedBox(height: 20),
_buildInfoCard(Icons.person, "الاسم", name),
const SizedBox(height: 12),
_buildInfoCard(Icons.badge, "المعرف", driverId),
const SizedBox(height: 12),
_buildInfoCard(Icons.speed, "السرعة", "$speed كم/س"),
const SizedBox(height: 20),
_buildStatsContainer(completed, cancelled),
if (isSuperAdmin) ...[
const SizedBox(height: 16),
_buildPhoneButton(phone),
],
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2C3E50),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 0,
),
child: const Text(
"إغلاق",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
)
],
),
),
),
),
);
}
Widget _buildInfoCard(IconData icon, String label, String value) {
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.grey.shade200,
width: 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 8,
)
],
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: const Color(0xFF2C3E50), size: 20),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
Text(
value,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: Color(0xFF2C3E50),
),
),
],
),
],
),
);
}
Widget _buildStatsContainer(String completed, String cancelled) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.grey.shade50, Colors.grey.shade100],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade200),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildStatItem("✓ مكتملة", completed, const Color(0xFF27AE60)),
Container(
width: 1,
height: 40,
color: Colors.grey.shade300,
),
_buildStatItem("✕ ملغاة", cancelled, const Color(0xFFE74C3C)),
],
),
);
}
Widget _buildStatItem(String label, String value, Color color) {
return Column(
children: [
Text(
value,
style: TextStyle(
color: color,
fontWeight: FontWeight.bold,
fontSize: 20,
),
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
],
);
}
Widget _buildPhoneButton(String phone) {
return InkWell(
onTap: () {
if (phone.isNotEmpty) _makePhoneCall(phone);
},
borderRadius: BorderRadius.circular(12),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 14),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [const Color(0xFFFFA500), const Color(0xFFFF8C00)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: const Color(0xFFFFA500).withOpacity(0.4),
blurRadius: 12,
spreadRadius: 2,
)
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.call, color: Colors.white, size: 20),
const SizedBox(width: 10),
Expanded(
child: Text(
phone,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 15,
),
textAlign: TextAlign.center,
),
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
appBar: AppBar(
elevation: 0,
backgroundColor: Colors.transparent,
centerTitle: true,
title: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFF2C3E50).withOpacity(0.9),
borderRadius: BorderRadius.circular(12),
// backdropFilter: const BackdropFilter(blur: 10),
),
child: const Text(
"نظام تتبع الكابتن",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
foregroundColor: Colors.white,
),
body: Stack(
children: [
FlutterMap(
mapController: _mapController,
options: const MapOptions(
initialCenter: LatLng(33.513, 36.276),
initialZoom: 10.0,
),
children: [
TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'com.tripz.app',
),
MarkerLayer(markers: _markers),
],
),
_buildDashboard(),
],
),
);
}
Widget _buildDashboard() {
return Positioned(
top: 100,
right: 16,
child: FadeTransition(
opacity: _fadeController,
child: ScaleTransition(
scale: _scaleController,
child: Container(
width: 300,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
blurRadius: 30,
spreadRadius: 5,
)
],
),
child: ClipRRect(
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)
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Icon(Icons.dashboard,
color: Colors.white, size: 22),
const Text(
"لوحة التحكم",
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
// Mode Buttons
Row(
children: [
Expanded(
child: _buildModeButton(
"أرشيف اليوم",
!isLiveMode,
() {
setState(() => isLiveMode = false);
fetchData();
},
const Color(0xFF3498DB),
),
),
const SizedBox(width: 10),
Expanded(
child: _buildModeButton(
"مباشر",
isLiveMode,
() {
setState(() => isLiveMode = true);
fetchData();
},
const Color(0xFF27AE60),
),
),
],
),
const SizedBox(height: 16),
// Stats
_buildStatRow(
icon: Icons.live_tv,
label: "نشط الآن (مباشر)",
value: liveCount.toString(),
color: const Color(0xFF27AE60),
),
const SizedBox(height: 12),
_buildStatRow(
icon: Icons.history,
label: "إجمالي اليوم",
value: dayCount.toString(),
color: const Color(0xFF3498DB),
),
const SizedBox(height: 14),
// Last Update
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade200),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
isLoading
? "جاري التحديث..."
: "تحديث: $lastUpdated",
style: TextStyle(
fontSize: 11,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
Icon(
isLoading
? Icons.hourglass_bottom
: Icons.check_circle,
size: 14,
color: isLoading ? Colors.orange : Colors.green,
),
],
),
),
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),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
elevation: 0,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (isLoading)
const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white,
),
),
)
else
const Icon(Icons.refresh, size: 18),
const SizedBox(width: 8),
const Text(
"تحديث البيانات",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
],
),
),
),
],
),
),
],
),
),
),
),
),
);
}
Widget _buildModeButton(
String title,
bool active,
VoidCallback onTap,
Color color,
) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(10),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
padding: const EdgeInsets.symmetric(vertical: 10),
alignment: Alignment.center,
decoration: BoxDecoration(
gradient: active
? LinearGradient(
colors: [color, color.withOpacity(0.8)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
)
: null,
color: active ? null : Colors.grey.shade100,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: active ? color : Colors.grey.shade300,
width: 1.5,
),
boxShadow: active
? [
BoxShadow(
color: color.withOpacity(0.3),
blurRadius: 8,
spreadRadius: 1,
)
]
: null,
),
child: Text(
title,
style: TextStyle(
color: active ? Colors.white : Colors.grey.shade700,
fontWeight: active ? FontWeight.bold : FontWeight.w600,
fontSize: 13,
),
),
),
);
}
Widget _buildStatRow({
required IconData icon,
required String label,
required String value,
required Color color,
}) {
return Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: color, size: 18),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
value,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: color,
),
),
],
),
),
],
);
}
}

View File

@@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_admin/constant/links.dart';
import 'package:siro_admin/controller/functions/crud.dart';
import 'package:siro_admin/controller/functions/wallet.dart';
import 'package:siro_admin/views/widgets/my_scafold.dart';
import '../../../controller/drivers/driverthebest.dart';
class DriverTheBestGiza extends StatelessWidget {
const DriverTheBestGiza({super.key});
@override
Widget build(BuildContext context) {
Get.put(DriverTheBestGizaController(), permanent: true);
return MyScafolld(
title: 'Giza'.tr,
body: [
GetBuilder<DriverTheBestGizaController>(builder: (driverthebest) {
return driverthebest.driver.isNotEmpty
? ListView.builder(
itemCount: driverthebest.driver.length,
itemBuilder: (context, index) {
final driver = driverthebest.driver[index];
return ListTile(
leading: CircleAvatar(
child: Text(
((driver['driver_count'] * 5) / 3600)
.toStringAsFixed(0),
),
),
title: Text(driver['name_arabic'] ?? 'Unknown Name'),
subtitle: Text('Phone: ${driver['phone'] ?? 'N/A'}'),
trailing: IconButton(
onPressed: () async {
Get.defaultDialog(
title:
'are you sure to pay to this driver gift'.tr,
middleText: '',
onConfirm: () async {
final wallet = Get.put(WalletController());
await wallet.addPaymentToDriver('100',
driver['id'].toString(), driver['token']);
await wallet.addSeferWallet(
'100', driver['id'].toString());
await CRUD().post(
link: AppLink.deleteRecord,
payload: {
'driver_id': driver['id'].toString()
});
driverthebest.driver.removeAt(index);
driverthebest.update();
},
onCancel: () => Get.back());
},
icon: const Icon(Icons.wallet_giftcard_rounded),
),
);
},
)
: const Center(
child: Text('No drivers available.'),
);
})
],
isleading: true,
);
}
}

View File

@@ -0,0 +1,833 @@
import 'dart:async';
import 'package:flutter/material.dart';
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';
/// --------------------------------------------------------------------------
/// 1. DATA MODELS
/// --------------------------------------------------------------------------
class DriverLocation {
final double latitude;
final double longitude;
final double speed;
final double heading;
final String updatedAt;
DriverLocation({
required this.latitude,
required this.longitude,
required this.speed,
required this.heading,
required this.updatedAt,
});
factory DriverLocation.fromJson(Map<String, dynamic> json) {
return DriverLocation(
latitude: double.tryParse(json['latitude'].toString()) ?? 0.0,
longitude: double.tryParse(json['longitude'].toString()) ?? 0.0,
speed: double.tryParse(json['speed'].toString()) ?? 0.0,
heading: double.tryParse(json['heading'].toString()) ?? 0.0,
updatedAt: json['updated_at'] ?? '',
);
}
}
/// --------------------------------------------------------------------------
/// 2. GETX CONTROLLER
/// --------------------------------------------------------------------------
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
// Map Variables
final MapController mapController = MapController();
Timer? _timer;
bool _isFirstLoad = true; // To trigger auto-fit bounds only on first success
@override
void onClose() {
_timer?.cancel();
phoneInputController.dispose();
super.onClose();
}
// --- ACTIONS ---
void startSearch() {
if (phoneInputController.text.trim().isEmpty) {
Get.snackbar(
"تنبيه",
"يرجى إدخال رقم الهاتف أولاً",
backgroundColor: Colors.redAccent.withOpacity(0.9),
colorText: Colors.white,
snackPosition: SnackPosition.TOP,
margin: const EdgeInsets.all(15),
borderRadius: 15,
);
return;
}
// Reset state
hasError.value = false;
errorMessage.value = '';
driverLocation.value = null;
startPoint.value = null;
endPoint.value = null;
routePolyline.clear();
driverName.value = "جاري التحميل...";
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();
});
}
void stopTracking() {
_timer?.cancel();
isTracking.value = false;
isLoading.value = false;
// phoneInputController.clear(); // اختياري: يمكنك إبقائه لتسهيل البحث مرة أخرى
}
Future<void> fetchRideData() async {
final phone = phoneInputController.text.trim();
if (phone.isEmpty) return;
try {
final response = await CRUD().post(
link: apiUrl,
payload: {"phone": "963$phone"},
);
if (response != 'failure') {
final jsonResponse = response;
if ((jsonResponse['message'] != null &&
jsonResponse['message'] != 'failure') ||
jsonResponse['status'] == 'success') {
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'];
LatLng? s = _parseLatLngString(startStr);
LatLng? e = _parseLatLngString(endStr);
if (s != null && e != null) {
startPoint.value = s;
endPoint.value = e;
routePolyline.value = [s, e]; // Straight line for now
}
}
// 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();
}
}
hasError.value = false;
} else {
hasError.value = true;
errorMessage.value = jsonResponse['message'] ??
"لم يتم العثور على رقم الهاتف أو لا توجد رحلة نشطة.";
}
} else {
hasError.value = true;
errorMessage.value = "فشل الاتصال بالخادم";
}
} catch (e) {
if (isLoading.value) {
hasError.value = true;
errorMessage.value = e.toString();
}
} finally {
isLoading.value = false;
}
}
// Helper to parse "lat,lng" string
LatLng? _parseLatLngString(String? str) {
if (str == null || !str.contains(',')) return null;
try {
final parts = str.split(',');
final lat = double.parse(parts[0].trim());
final lng = double.parse(parts[1].trim());
return LatLng(lat, lng);
} catch (e) {
return null;
}
}
// Logic to fit start, end, and driver on screen
void _updateMapBounds() {
if (!_isFirstLoad) return;
List<LatLng> pointsToFit = [];
if (startPoint.value != null) pointsToFit.add(startPoint.value!);
if (endPoint.value != null) pointsToFit.add(endPoint.value!);
if (driverLocation.value != null) {
pointsToFit.add(LatLng(
driverLocation.value!.latitude, driverLocation.value!.longitude));
}
if (pointsToFit.isNotEmpty) {
try {
final bounds = LatLngBounds.fromPoints(pointsToFit);
mapController.fitCamera(
CameraFit.bounds(
bounds: bounds,
padding: const EdgeInsets.all(80.0),
),
);
_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 RideMonitorController controller = Get.put(RideMonitorController());
return Scaffold(
backgroundColor: backgroundColor,
// الإبقاء على AppBar فقط في شاشة البحث
appBar: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight),
child: Obx(() {
if (controller.isTracking.value)
return const SizedBox
.shrink(); // إخفاء الـ AppBar في وضع التتبع للخريطة الكاملة
return AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
centerTitle: true,
iconTheme: IconThemeData(color: textPrimary),
title: Text(
"مراقبة الرحلات",
style: TextStyle(
color: textPrimary,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
);
}),
),
body: Obx(() {
if (!controller.isTracking.value) {
return _buildSearchForm(context, controller);
}
return _buildMapTrackingView(context, controller);
}),
);
}
// ---------------------------------------------------------------------------
// واجهة البحث (Search View)
// ---------------------------------------------------------------------------
Widget _buildSearchForm(
BuildContext context, RideMonitorController controller) {
return Center(
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Container(
padding: const EdgeInsets.all(32.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: primaryColor.withOpacity(0.08),
blurRadius: 24,
offset: const Offset(0, 10),
)
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: primaryColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child:
Icon(Icons.radar_rounded, size: 60, color: primaryColor),
),
const SizedBox(height: 24),
Text(
"تتبع رحلة نشطة",
style: TextStyle(
color: textPrimary,
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
"أدخل رقم هاتف السائق أو الراكب للبدء",
style: TextStyle(
color: textSecondary,
fontSize: 14,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
Container(
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.white, width: 2),
),
child: TextField(
controller: controller.phoneInputController,
keyboardType: TextInputType.phone,
textDirection: TextDirection.ltr,
style: TextStyle(
color: textPrimary,
fontSize: 16,
fontWeight: FontWeight.w600,
),
decoration: InputDecoration(
hintText: "مثال: 0992952235...",
hintStyle: TextStyle(color: textSecondary),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(
vertical: 18, horizontal: 20),
prefixIcon:
Icon(Icons.phone_rounded, color: primaryColor),
),
),
),
const SizedBox(height: 32),
SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton(
onPressed: controller.startSearch,
style: ElevatedButton.styleFrom(
backgroundColor: primaryColor,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: const Text(
"بدء المراقبة",
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
),
),
);
}
// ---------------------------------------------------------------------------
// واجهة الخريطة (Map View)
// ---------------------------------------------------------------------------
Widget _buildMapTrackingView(
BuildContext context, RideMonitorController controller) {
return Stack(
children: [
FlutterMap(
mapController: controller.mapController,
options: MapOptions(
initialCenter: const LatLng(30.0444, 31.2357),
initialZoom: 12.0,
),
children: [
TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'com.sefer.admin',
),
// 1. ROUTE LINE (Polyline)
if (controller.routePolyline.isNotEmpty)
PolylineLayer(
polylines: [
Polyline(
points: controller.routePolyline.value,
strokeWidth: 6.0,
color: primaryColor.withOpacity(0.9),
borderStrokeWidth: 2.0,
borderColor: primaryColor.withOpacity(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)),
),
// End Point (Red Dot)
if (controller.endPoint.value != null)
Marker(
point: controller.endPoint.value!,
width: 30,
height: 30,
child: _buildPointMarker(const Color(0xFFEF4444)),
),
// Driver Car Marker
if (controller.driverLocation.value != null)
Marker(
point: LatLng(
controller.driverLocation.value!.latitude,
controller.driverLocation.value!.longitude,
),
width: 80,
height: 80,
child: Transform.rotate(
angle: (controller.driverLocation.value!.heading *
(3.14159 / 180)),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 10,
spreadRadius: 2,
)
],
),
child: Icon(
Icons.directions_car_rounded,
color: primaryColor,
size: 28,
),
),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: textPrimary,
borderRadius: BorderRadius.circular(6),
),
child: Text(
"${controller.driverLocation.value!.speed.toInt()} كم",
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
textDirection: TextDirection.rtl,
),
)
],
),
),
),
],
),
],
),
// زر التراجع (إيقاف التتبع) أعلى الشاشة
Positioned(
top: MediaQuery.of(context).padding.top + 10,
right: 20, // أو left حسب لغة التطبيق
child: Container(
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
)
],
),
child: IconButton(
icon: Icon(Icons.close_rounded, color: textPrimary, 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),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(
color: primaryColor, strokeWidth: 3),
const SizedBox(height: 16),
Text(
"جاري تحديد الموقع...",
style: TextStyle(
color: textPrimary,
fontWeight: FontWeight.bold,
fontSize: 16,
),
)
],
),
),
),
// ERROR OVERLAY
if (controller.hasError.value)
Center(
child: Container(
margin: const EdgeInsets.all(24),
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 20,
offset: const Offset(0, 10),
)
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
shape: BoxShape.circle,
),
child: const Icon(Icons.error_outline_rounded,
color: Colors.red, size: 40),
),
const SizedBox(height: 16),
Text(
"حدث خطأ",
style: TextStyle(
color: textPrimary,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
controller.errorMessage.value,
textAlign: TextAlign.center,
style: TextStyle(color: textSecondary, height: 1.5),
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: controller.stopTracking,
style: ElevatedButton.styleFrom(
backgroundColor: backgroundColor,
foregroundColor: textPrimary,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(vertical: 14),
),
child: const Text("رجوع للبحث",
style: TextStyle(fontWeight: FontWeight.bold)),
),
)
],
),
),
),
// INFO CARD (Bottom Floating Card)
if (!controller.hasError.value && !controller.isLoading.value)
Positioned(
bottom: 30,
left: 20,
right: 20,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 24,
offset: const Offset(0, 10),
)
],
),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(15),
),
child: Icon(Icons.person_rounded,
color: primaryColor, size: 28),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
controller.driverName.value,
style: TextStyle(
color: textPrimary,
fontSize: 18,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Row(
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: controller.rideStatus.value
.toLowerCase() ==
'begin'
? const Color(0xFF10B981)
: const Color(0xFFF59E0B),
),
),
const SizedBox(width: 6),
Text(
controller.rideStatus.value,
style: TextStyle(
color: textSecondary,
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
],
),
],
),
),
],
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Divider(height: 1, thickness: 1),
),
if (controller.driverLocation.value != null)
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildModernInfoBadge(
Icons.speed_rounded,
"${controller.driverLocation.value!.speed.toStringAsFixed(1)} كم/س",
const Color(0xFF3B82F6),
),
Container(
width: 1,
height: 30,
color: Colors.grey.withOpacity(0.2)),
_buildModernInfoBadge(
Icons.access_time_rounded,
controller.driverLocation.value!.updatedAt
.split(' ')
.last,
const Color(0xFF8B5CF6),
),
],
)
else
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
color: primaryColor, strokeWidth: 2),
),
const SizedBox(width: 10),
Text(
"جاري الاتصال بالسائق...",
style: TextStyle(
color: primaryColor,
fontWeight: FontWeight.w600,
fontSize: 13,
),
),
],
),
],
),
),
),
),
],
);
}
// --- Helper Widgets ---
Widget _buildPointMarker(Color color) {
return Container(
decoration: BoxDecoration(
color: color.withOpacity(0.3),
shape: BoxShape.circle,
),
child: Center(
child: Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: [
BoxShadow(
color: color.withOpacity(0.5),
blurRadius: 6,
spreadRadius: 1,
)
],
),
),
),
);
}
Widget _buildModernInfoBadge(IconData icon, String text, Color iconColor) {
return Row(
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: iconColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, size: 16, color: iconColor),
),
const SizedBox(width: 8),
Text(
text,
style: TextStyle(
color: textPrimary,
fontSize: 14,
fontWeight: FontWeight.bold,
),
textDirection: TextDirection.ltr, // للحفاظ على اتجاه الأرقام
),
],
);
}
}

View File

@@ -0,0 +1,564 @@
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:url_launcher/url_launcher.dart';
class EmployeePage extends StatelessWidget {
const EmployeePage({super.key});
@override
Widget build(BuildContext context) {
// حقن الكنترولر
Get.put(EmployeeController());
// ألوان الثيم
const Color bgColor = Color(0xFF0A0E27);
const Color cardColor = Color(0xFF1A1F3A);
const Color primaryAccent = Color(0xFF6366F1);
return Scaffold(
backgroundColor: bgColor,
body: GetBuilder<EmployeeController>(
builder: (controller) {
return CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: [
// 1. App Bar
SliverAppBar(
expandedHeight: 100,
floating: true,
pinned: true,
backgroundColor: bgColor,
elevation: 0,
flexibleSpace: FlexibleSpaceBar(
titlePadding: const EdgeInsets.only(bottom: 16),
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: primaryAccent.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(Icons.badge_rounded,
color: Colors.white, size: 18),
),
const SizedBox(width: 10),
const Text(
'الموظفون',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Colors.white,
fontFamily: 'Segoe UI',
),
),
],
),
centerTitle: true,
background: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
primaryAccent.withOpacity(0.15),
bgColor,
],
),
),
),
),
),
// 2. قائمة الموظفين
if (controller.employee.isEmpty)
const SliverFillRemaining(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.people_outline,
size: 60, color: Colors.white24),
SizedBox(height: 16),
Text("لا يوجد موظفين حالياً",
style: TextStyle(color: Colors.white54)),
],
),
),
)
else
SliverPadding(
padding: const EdgeInsets.all(16),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final employee = controller.employee[index];
return _EmployeeCard(
employee: employee,
index: index,
cardColor: cardColor,
primaryAccent: primaryAccent,
);
},
childCount: controller.employee.length,
),
),
),
],
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
final controller = Get.find<EmployeeController>();
controller.id = controller.generateRandomId(8);
Get.to(() => _EmployeeFormScreen(controller: controller));
},
backgroundColor: primaryAccent,
child: const Icon(Icons.person_add_rounded, color: Colors.white),
),
);
}
}
// === بطاقة الموظف (تصميم جديد ومحسن) ===
class _EmployeeCard extends StatelessWidget {
final Map<String, dynamic> employee;
final int index;
final Color cardColor;
final Color primaryAccent;
const _EmployeeCard({
required this.employee,
required this.index,
required this.cardColor,
required this.primaryAccent,
});
@override
Widget build(BuildContext context) {
bool isExcellent = employee['status'].toString().contains('ممتاز');
Color statusColor = isExcellent ? const Color(0xFF10B981) : Colors.amber;
return Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: cardColor,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.white.withOpacity(0.05)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => Get.to(() => EmployeeDetails(index: index)),
borderRadius: BorderRadius.circular(16),
child: Padding(
padding: const EdgeInsets.all(20),
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),
shape: BoxShape.circle,
),
child: Icon(Icons.person,
color: Colors.white.withOpacity(0.7), size: 20),
),
Flexible(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border:
Border.all(color: statusColor.withOpacity(0.3)),
),
child: Text(
employee['status'] ?? 'Unknown',
style: TextStyle(
color: statusColor,
fontSize: 12,
fontWeight: FontWeight.bold,
height: 1.5,
),
textAlign: TextAlign.center,
),
),
),
],
),
const SizedBox(height: 12),
// الاسم في سطر كامل ومميز
Text(
employee['name'] ?? 'Unknown',
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
height: 1.3,
),
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 12),
child: Divider(height: 1, color: Colors.white10),
),
// تفاصيل التعليم والهاتف والموقع (مع دعم تعدد الأسطر)
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow(Icons.phone_iphone_rounded,
employee['phone'] ?? '', Colors.white54),
const SizedBox(height: 12), // مسافة أكبر بين العناصر
_buildInfoRow(
Icons.school_outlined,
employee['education'] ?? 'غير محدد',
primaryAccent),
const SizedBox(height: 12), // مسافة أكبر
_buildInfoRow(Icons.location_on_outlined,
employee['site'] ?? 'غير محدد', Colors.blueGrey),
],
),
),
// زر الاتصال الجانبي
const SizedBox(width: 16),
Material(
color: Colors.green.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
child: InkWell(
onTap: () =>
_makePhoneCall(employee['phone'].toString()),
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(14),
child: const Icon(Icons.call,
color: Colors.green, size: 24),
),
),
),
],
),
],
),
),
),
),
);
}
Widget _buildInfoRow(IconData icon, String text, Color iconColor) {
return Row(
crossAxisAlignment:
CrossAxisAlignment.start, // محاذاة الأيقونة مع بداية النص
children: [
Padding(
padding: const EdgeInsets.only(top: 2), // ضبط بسيط لموقع الأيقونة
child: Icon(icon, size: 16, color: iconColor.withOpacity(0.8)),
),
const SizedBox(width: 10),
Expanded(
child: Text(
text,
style: TextStyle(
color: Colors.white.withOpacity(0.7),
fontSize: 13,
height: 1.5, // تباعد الأسطر لسهولة القراءة
),
// تم إزالة maxLines و overflow للسماح بالنص بالنزول لأسطر متعددة
),
),
],
);
}
Future<void> _makePhoneCall(String phoneNumber) async {
final Uri launchUri = Uri(scheme: 'tel', path: phoneNumber);
if (await canLaunchUrl(launchUri)) {
await launchUrl(launchUri);
}
}
}
// === شاشة إضافة موظف ===
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);
return Scaffold(
backgroundColor: bgColor,
appBar: AppBar(
title: const Text("إضافة موظف جديد",
style: TextStyle(color: Colors.white)),
backgroundColor: bgColor,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Get.back(),
),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Form(
key: controller.formKey,
child: Column(
children: [
Row(
children: [
Expanded(
child: _UploadButton(
title: "الهوية (أمام)",
icon: Icons.credit_card,
onPressed: () async {
await ImageController().choosImage(AppLink.uploadEgypt,
'idFrontEmployee', controller.id);
},
),
),
const SizedBox(width: 16),
Expanded(
child: _UploadButton(
title: "الهوية (خلف)",
icon: Icons.credit_card_outlined,
onPressed: () async {
await ImageController().choosImage(AppLink.uploadEgypt,
'idbackEmployee', controller.id);
},
),
),
],
),
const SizedBox(height: 24),
_buildModernTextField(
controller.name, "الاسم الكامل", Icons.person, inputColor),
const SizedBox(height: 16),
_buildModernTextField(
controller.phone, "رقم الهاتف", Icons.phone, inputColor,
type: TextInputType.phone),
const SizedBox(height: 16),
_buildModernTextField(controller.education, "التعليم / الملاحظات",
Icons.school, inputColor),
const SizedBox(height: 16),
_buildModernTextField(controller.site, "الموقع / العنوان",
Icons.location_on, inputColor),
const SizedBox(height: 16),
_buildModernTextField(controller.status, "الحالة (مثال: ممتاز)",
Icons.star, inputColor),
const SizedBox(height: 32),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () async {
if (controller.formKey.currentState!.validate()) {
await controller.addEmployee();
Get.back();
}
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF6366F1),
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
child: const Text("حفظ البيانات",
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold)),
),
),
],
),
),
),
);
}
Widget _buildModernTextField(TextEditingController controller, String hint,
IconData icon, Color fillColor,
{TextInputType type = TextInputType.text}) {
return Container(
decoration: BoxDecoration(
color: fillColor,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white.withOpacity(0.1)),
),
child: TextFormField(
controller: controller,
keyboardType: type,
style: const TextStyle(color: Colors.white),
maxLines: null, // السماح بتعدد الأسطر عند الإدخال أيضاً
decoration: InputDecoration(
labelText: hint,
labelStyle: TextStyle(color: Colors.white.withOpacity(0.5)),
prefixIcon: Icon(icon, color: Colors.white38, size: 20),
border: InputBorder.none,
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
),
validator: (value) =>
value == null || value.isEmpty ? 'حقل مطلوب' : null,
),
);
}
}
class _UploadButton extends StatelessWidget {
final String title;
final IconData icon;
final VoidCallback onPressed;
const _UploadButton(
{required this.title, required this.icon, required this.onPressed});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 20),
decoration: BoxDecoration(
color: const Color(0xFF1A1F3A),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0xFF6366F1).withOpacity(0.3),
style: BorderStyle.solid),
),
child: Column(
children: [
Icon(icon, color: const Color(0xFF6366F1), size: 30),
const SizedBox(height: 8),
Text(title,
style: const TextStyle(color: Colors.white70, fontSize: 12)),
const SizedBox(height: 4),
const Text("اضغط للرفع",
style: TextStyle(color: Colors.white38, fontSize: 10)),
],
),
),
);
}
}
// === شاشة التفاصيل ===
class EmployeeDetails extends StatelessWidget {
final int index;
const EmployeeDetails({super.key, required this.index});
@override
Widget build(BuildContext context) {
const Color bgColor = Color(0xFF0A0E27);
return Scaffold(
backgroundColor: bgColor,
appBar: AppBar(
title:
const Text('تفاصيل الموظف', style: TextStyle(color: Colors.white)),
backgroundColor: bgColor,
iconTheme: const IconThemeData(color: Colors.white),
elevation: 0,
),
body: GetBuilder<EmployeeController>(
builder: (controller) {
final employeeId = controller.employee[index]['id'];
return SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("الهوية الأمامية",
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold)),
const SizedBox(height: 10),
_buildImageViewer(
'${AppLink.server}/card_image/idFrontEmployee-$employeeId.jpg',
),
const SizedBox(height: 30),
const Text("الهوية الخلفية",
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold)),
const SizedBox(height: 10),
_buildImageViewer(
'${AppLink.server}/card_image/idbackEmployee-$employeeId.jpg',
),
],
),
);
},
),
);
}
Widget _buildImageViewer(String url) {
return Container(
width: double.infinity,
height: 220,
decoration: BoxDecoration(
color: const Color(0xFF1A1F3A),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.white.withOpacity(0.1)),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Image.network(
url,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.broken_image_rounded,
color: Colors.white24, size: 50),
SizedBox(height: 8),
Text("فشل تحميل الصورة",
style: TextStyle(color: Colors.white24)),
],
),
);
},
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return const Center(
child: CircularProgressIndicator(color: Color(0xFF6366F1)));
},
),
),
);
}
}

View File

@@ -0,0 +1,266 @@
// ═══════════════════════════════════════════════════════════════
// driver_fingerprint_migration.dart
// ───────────────────────────────────────────────────────────────
// المنطق ببساطة:
// 1. خذ البصمة كما هي من DB
// 2. split('_') → احذف آخر جزء (OS version)
// 3. join('_') → encrypt → رفع
//
// مثال:
// "abc123_SamsungA51_13" → "abc123_SamsungA51" → encrypt
// "TECNO_LH7n-GL_14" → "TECNO_LH7n-GL" → encrypt
// "unknown_2412DPC0AG_15" → "unknown_2412DPC0AG" → encrypt
// ═══════════════════════════════════════════════════════════════
import 'package:flutter/material.dart';
import '../../../constant/links.dart';
import '../../../controller/functions/crud.dart';
import '../../../controller/functions/encrypt_decrypt.dart';
import '../../../print.dart';
class DriverFingerprintMigrationTool extends StatefulWidget {
const DriverFingerprintMigrationTool({super.key});
@override
State<DriverFingerprintMigrationTool> createState() =>
_DriverFingerprintMigrationToolState();
}
class _DriverFingerprintMigrationToolState
extends State<DriverFingerprintMigrationTool> {
bool _isRunning = false;
bool _isDone = false;
int _total = 0;
int _processed = 0;
int _updated = 0;
int _failed = 0;
String _currentLog = '';
static const int _batchSize = 50;
// ─────────────────────────────────────────────────────────────
// المنطق الأساسي — حذف آخر جزء بعد "_"
// ─────────────────────────────────────────────────────────────
String _removeLastSegment(String raw) {
final parts = raw.split('_');
if (parts.length <= 1) return raw; // جزء واحد — ما في شيء نحذفه
parts.removeLast();
return parts.join('_');
}
Future<void> _startMigration() async {
setState(() {
_isRunning = true;
_isDone = false;
_processed = 0;
_updated = 0;
_failed = 0;
_currentLog = 'جارٍ جلب بصمات السائقين...';
});
try {
final records = await _fetchAll();
if (records == null) {
_log('❌ فشل في جلب البيانات');
setState(() => _isRunning = false);
return;
}
_total = records.length;
_log('✅ تم جلب $_total بصمة — بدء المعالجة...');
for (int i = 0; i < records.length; i += _batchSize) {
final batch = records.skip(i).take(_batchSize).toList();
_log('⚙️ معالجة ${i + 1}${i + batch.length} من $_total');
await Future.wait(batch.map(_processSingle));
if (i + _batchSize < records.length) {
await Future.delayed(const Duration(milliseconds: 300));
}
}
_log('🎉 اكتمل!\nمحدَّث: $_updated | فاشل: $_failed');
setState(() {
_isDone = true;
_isRunning = false;
});
} catch (e) {
_log('❌ خطأ: $e');
setState(() => _isRunning = false);
}
}
Future<List<Map<String, dynamic>>?> _fetchAll() async {
try {
final response = await CRUD().post(
link: AppLink.getAllDriverFingerprints,
payload: {'admin_key': 'iuyweiruinakjbfkajkjlkmalkcxnlahd'},
);
if (response == 'failure' || response == null) return null;
final data = response['data'];
if (data is! List) return null;
return List<Map<String, dynamic>>.from(data);
} catch (e) {
Log.print('fetchAll error: $e');
return null;
}
}
Future<void> _processSingle(Map<String, dynamic> record) async {
final captainId = record['captain_id']?.toString() ?? '';
final rawFp = record['fingerPrint']?.toString() ?? '';
if (captainId.isEmpty || rawFp.isEmpty) {
setState(() {
_failed++;
_processed++;
});
return;
}
try {
// ── حذف آخر جزء (OS version) ─────────────────────────────
final String newRaw = _removeLastSegment(rawFp);
final String encrypted = EncryptionHelper.instance.encryptData(newRaw);
Log.print('🔄 [$captainId] "$rawFp" → "$newRaw" → encrypted');
// ── رفع للسيرفر ──────────────────────────────────────────
final res = await CRUD().post(
link: AppLink.updateDriverFingerprintAdmin,
payload: {
'captain_id': captainId,
'fingerprint': encrypted,
'admin_key': 'iuyweiruinakjbfkajkjlkmalkcxnlahd',
},
);
if (res != 'failure' && res?['status'] == 'success') {
setState(() {
_updated++;
_processed++;
});
} else {
setState(() {
_failed++;
_processed++;
});
}
} catch (e) {
Log.print('❌ [$captainId]: $e');
setState(() {
_failed++;
_processed++;
});
}
}
void _log(String msg) {
Log.print(msg);
setState(() => _currentLog = msg);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Driver FP Migration')),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.orange.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.orange.shade200),
),
child: const Text(
'⚠️ تُستخدم مرة واحدة فقط\n\n'
'"abc123_Samsung_13" → "abc123_Samsung" → encrypt\n'
'"TECNO_LH7n_14" → "TECNO_LH7n" → encrypt',
style:
TextStyle(fontSize: 13, height: 1.7, fontFamily: 'monospace'),
),
),
const SizedBox(height: 24),
if (_total > 0) ...[
Text('التقدم: $_processed / $_total',
style: const TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
LinearProgressIndicator(
value: _total > 0 ? _processed / _total : 0,
backgroundColor: Colors.grey.shade200,
color: _isDone ? Colors.green : Colors.blue,
minHeight: 8,
),
const SizedBox(height: 16),
],
if (_processed > 0)
Row(children: [
_chip('محدَّث', _updated, Colors.green),
const SizedBox(width: 8),
_chip('فاشل', _failed, Colors.red),
]),
const SizedBox(height: 16),
if (_currentLog.isNotEmpty)
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Text(_currentLog,
style:
const TextStyle(fontFamily: 'monospace', fontSize: 12)),
),
const Spacer(),
SizedBox(
width: double.infinity,
height: 52,
child: ElevatedButton(
onPressed: (_isRunning || _isDone) ? null : _startMigration,
style: ElevatedButton.styleFrom(
backgroundColor: _isDone ? Colors.green : Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
child: _isRunning
? const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: Colors.white, strokeWidth: 2)),
SizedBox(width: 12),
Text('جارٍ الترحيل...',
style:
TextStyle(color: Colors.white, fontSize: 16)),
],
)
: Text(
_isDone ? '✅ اكتمل الترحيل' : 'بدء ترحيل بصمات السائقين',
style: const TextStyle(color: Colors.white, fontSize: 16),
),
),
),
]),
),
);
}
Widget _chip(String label, int value, Color color) => Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: color.withOpacity(0.3)),
),
child: Text('$label: $value',
style: TextStyle(color: color, fontWeight: FontWeight.bold)),
);
}

View File

@@ -0,0 +1,855 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:siro_admin/constant/links.dart';
import 'package:siro_admin/controller/functions/crud.dart';
import 'package:siro_admin/main.dart';
import '../../../constant/box_name.dart';
// ─── Custom Colors ────────────────────────────────────────────────────────────
class _AppColors {
static const bg = Color(0xFF0A0D14);
static const surface = Color(0xFF111622);
static const card = Color(0xFF161D2E);
static const border = Color(0xFF1F2D4A);
static const accent = Color(0xFF00E5FF);
static const accentDim = Color(0xFF0097A7);
static const accentGlow = Color(0x2200E5FF);
static const accentDecrypt = Color(0xFF7C4DFF);
static const accentDecryptDim = Color(0xFF512DA8);
static const accentDecryptGlow = Color(0x227C4DFF);
static const textPrimary = Color(0xFFE8F0FE);
static const textSec = Color(0xFF7A8BAA);
static const success = Color(0xFF00E676);
static const error = Color(0xFFFF5252);
}
class EncryptToolPage extends StatefulWidget {
final String adminToken;
const EncryptToolPage({Key? key, required this.adminToken}) : super(key: key);
@override
State<EncryptToolPage> createState() => _EncryptToolPageState();
}
class _EncryptToolPageState extends State<EncryptToolPage>
with SingleTickerProviderStateMixin {
final TextEditingController _inputController = TextEditingController();
final TextEditingController _outputController = TextEditingController();
String _output = '';
bool _loading = false;
String? _error;
bool _isInputCopied = false;
bool _isOutputCopied = false;
late final AnimationController _glowController;
late final Animation<double> _glowAnimation;
@override
void initState() {
super.initState();
_glowController = AnimationController(
vsync: this,
duration: const Duration(seconds: 3),
)..repeat(reverse: true);
_glowAnimation = Tween<double>(begin: 0.4, end: 1.0).animate(
CurvedAnimation(parent: _glowController, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_inputController.dispose();
_outputController.dispose();
_glowController.dispose();
super.dispose();
}
// ─── Logic (unchanged) ──────────────────────────────────────────────────────
Future<void> _callTool(String action) async {
FocusScope.of(context).unfocus();
setState(() {
_loading = true;
_error = null;
_output = '';
_outputController.clear();
_isInputCopied = false;
_isOutputCopied = false;
});
try {
final response = await CRUD().post(
link: '${AppLink.server}/ggg.php',
payload: {
'action': action,
'text': _inputController.text,
'admin_phone': box.read(BoxName.adminPhone) ?? '',
},
);
if (response == 'failure') {
setState(() => _error = 'حدث خطأ في الاتصال بالخادم. حاول مرة أخرى.');
} else {
if (response['status'] == 'success') {
setState(() {
_output = (response['result'] ?? '').toString();
_outputController.text = _output;
});
} else {
setState(() =>
_error = response['message']?.toString() ?? 'حدث خطأ غير معروف.');
}
}
} catch (e) {
setState(() => _error = 'مشكلة في الشبكة: $e');
} finally {
setState(() => _loading = false);
}
}
void _showSnackBar(String message, {bool isError = false}) {
ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Icon(isError ? Icons.error_outline : Icons.check_circle,
color: isError ? _AppColors.error : _AppColors.success),
const SizedBox(width: 12),
Text(
message,
style: const TextStyle(
fontFamily: 'Cairo',
fontWeight: FontWeight.bold,
color: _AppColors.textPrimary,
),
),
],
),
backgroundColor:
isError ? const Color(0xFF1A0808) : const Color(0xFF081A0F),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
side: BorderSide(
color: isError ? _AppColors.error : _AppColors.success,
width: 1,
),
),
margin: const EdgeInsets.all(16),
duration: const Duration(seconds: 2),
),
);
}
Future<void> _copyText(bool isInput) async {
final textToCopy = isInput ? _inputController.text : _outputController.text;
if (textToCopy.isEmpty) {
_showSnackBar('لا يوجد نص لنسخه!', isError: true);
return;
}
await Clipboard.setData(ClipboardData(text: textToCopy));
if (isInput) {
setState(() => _isInputCopied = true);
Future.delayed(const Duration(seconds: 2), () {
if (mounted) setState(() => _isInputCopied = false);
});
} else {
setState(() => _isOutputCopied = true);
Future.delayed(const Duration(seconds: 2), () {
if (mounted) setState(() => _isOutputCopied = false);
});
}
_showSnackBar('تم النسخ بنجاح!');
}
Future<void> _pasteText(bool isInput) async {
final clipboardData = await Clipboard.getData(Clipboard.kTextPlain);
final textToPaste = clipboardData?.text ?? '';
if (textToPaste.isEmpty) {
_showSnackBar('الحافظة فارغة!', isError: true);
return;
}
setState(() {
if (isInput) {
_inputController.text = textToPaste;
} else {
_output = textToPaste;
_outputController.text = textToPaste;
}
});
_showSnackBar('تم اللصق بنجاح!');
}
// ─── UI Helpers ─────────────────────────────────────────────────────────────
Widget _buildActionButtons({
required bool isInput,
required bool isCopied,
Color accentColor = _AppColors.accent,
}) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
_MiniIconButton(
label: 'لصق',
icon: Icons.content_paste_rounded,
color: accentColor,
onTap: () => _pasteText(isInput),
),
const SizedBox(width: 6),
_MiniIconButton(
label: isCopied ? 'تم!' : 'نسخ',
icon: isCopied ? Icons.check_circle_rounded : Icons.copy_rounded,
color: isCopied ? _AppColors.success : accentColor,
onTap: () => _copyText(isInput),
),
],
);
}
InputDecoration _buildInputDecoration(String hint) => InputDecoration(
hintText: hint,
hintStyle: const TextStyle(color: _AppColors.textSec, fontSize: 14),
filled: true,
fillColor: const Color(0xFF0C1120),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: _AppColors.border, width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: _AppColors.accent, width: 1.5),
),
contentPadding: const EdgeInsets.all(16),
);
// ─── Build ──────────────────────────────────────────────────────────────────
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: _AppColors.bg,
body: Stack(
children: [
// ── Ambient background glow ──────────────────────────────────────────
Positioned(
top: -120,
left: -80,
child: AnimatedBuilder(
animation: _glowAnimation,
builder: (_, __) => Opacity(
opacity: _glowAnimation.value * 0.25,
child: Container(
width: 380,
height: 380,
decoration: const BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [Color(0xFF00E5FF), Colors.transparent],
),
),
),
),
),
),
Positioned(
bottom: -100,
right: -60,
child: AnimatedBuilder(
animation: _glowAnimation,
builder: (_, __) => Opacity(
opacity: (1 - _glowAnimation.value) * 0.2,
child: Container(
width: 320,
height: 320,
decoration: const BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [Color(0xFF7C4DFF), Colors.transparent],
),
),
),
),
),
),
// ── Content ──────────────────────────────────────────────────────────
SafeArea(
child: Column(
children: [
// ── AppBar ─────────────────────────────────────────────────────
Padding(
padding: const EdgeInsets.fromLTRB(8, 8, 16, 0),
child: Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_back_ios_new_rounded,
color: _AppColors.textSec, size: 20),
onPressed: () => Navigator.pop(context),
),
const Spacer(),
Row(
children: [
Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: _AppColors.accent,
boxShadow: [
BoxShadow(
color: _AppColors.accentGlow,
blurRadius: 8,
spreadRadius: 2,
),
],
),
),
const SizedBox(width: 10),
const Text(
'أداة التشفير',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: _AppColors.textPrimary,
letterSpacing: 0.5,
),
),
],
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: _AppColors.accentGlow,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: _AppColors.accent.withOpacity(0.3)),
),
child: const Text(
'AES-256',
style: TextStyle(
color: _AppColors.accent,
fontSize: 11,
fontWeight: FontWeight.bold,
letterSpacing: 1.5,
),
),
),
],
),
),
// ── Scrollable body ───────────────────────────────────────────
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 24, 20, 32),
physics: const BouncingScrollPhysics(),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 650),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// ─── Input Card ─────────────────────────────────────
_GlassCard(
borderColor: _AppColors.accent.withOpacity(0.2),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: _AppColors.accentGlow,
borderRadius:
BorderRadius.circular(10),
),
child: const Icon(
Icons.text_fields_rounded,
color: _AppColors.accent,
size: 18),
),
const SizedBox(width: 12),
const Text(
'النص الأصلي',
style: TextStyle(
color: _AppColors.textPrimary,
fontSize: 15,
fontWeight: FontWeight.w700,
),
),
],
),
_buildActionButtons(
isInput: true,
isCopied: _isInputCopied,
accentColor: _AppColors.accent,
),
],
),
const SizedBox(height: 16),
// Input field
TextField(
controller: _inputController,
maxLines: 5,
minLines: 3,
textDirection: TextDirection.ltr,
style: const TextStyle(
color: _AppColors.textPrimary,
fontSize: 15,
height: 1.6,
fontFamily: 'monospace',
),
onTap: () {
if (_inputController.text.isNotEmpty) {
_inputController.selection =
TextSelection(
baseOffset: 0,
extentOffset:
_inputController.text.length,
);
}
},
decoration: _buildInputDecoration(
'اكتب أو الصق النص هنا...'),
),
const SizedBox(height: 24),
// Action buttons row
Row(
children: [
// Encrypt
Expanded(
child: _ActionButton(
label: 'تشفير',
icon: Icons.lock_rounded,
isLoading: _loading,
onPressed: _loading
? null
: () => _callTool('encrypt'),
gradient: const LinearGradient(
colors: [
Color(0xFF00B4D8),
Color(0xFF00E5FF)
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
glowColor: _AppColors.accentGlow,
borderColor: _AppColors.accent,
),
),
const SizedBox(width: 14),
// Decrypt
Expanded(
child: _ActionButton(
label: 'فك التشفير',
icon: Icons.lock_open_rounded,
isLoading: _loading,
onPressed: _loading
? null
: () => _callTool('decrypt'),
gradient: const LinearGradient(
colors: [
Color(0xFF5B2EA6),
Color(0xFF7C4DFF)
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
glowColor:
_AppColors.accentDecryptGlow,
borderColor: _AppColors.accentDecrypt,
),
),
],
),
],
),
),
// ─── Error message ───────────────────────────────────
AnimatedSize(
duration: const Duration(milliseconds: 300),
child: _error != null
? Container(
margin: const EdgeInsets.only(top: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF1A0808),
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: _AppColors.error
.withOpacity(0.4)),
),
child: Row(
children: [
const Icon(
Icons.error_outline_rounded,
color: _AppColors.error,
size: 20),
const SizedBox(width: 12),
Expanded(
child: Text(
_error!,
style: const TextStyle(
color: _AppColors.error,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
],
),
)
: const SizedBox.shrink(),
),
// ─── Output Card ─────────────────────────────────────
const SizedBox(height: 20),
_GlassCard(
borderColor:
_AppColors.accentDecrypt.withOpacity(0.25),
headerWidget: Container(
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 14),
decoration: const BoxDecoration(
color: Color(0xFF16102A),
borderRadius: BorderRadius.vertical(
top: Radius.circular(20)),
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: _AppColors.accentDecryptGlow,
borderRadius:
BorderRadius.circular(10),
),
child: const Icon(
Icons.shield_rounded,
color: _AppColors.accentDecrypt,
size: 18),
),
const SizedBox(width: 12),
const Text(
'النتيجة',
style: TextStyle(
color: _AppColors.textPrimary,
fontSize: 15,
fontWeight: FontWeight.w700,
),
),
],
),
_buildActionButtons(
isInput: false,
isCopied: _isOutputCopied,
accentColor: _AppColors.accentDecrypt,
),
],
),
),
child: Padding(
padding: const EdgeInsets.all(20),
child: TextField(
controller: _outputController,
readOnly: true,
maxLines: 5,
minLines: 3,
textDirection: TextDirection.ltr,
style: const TextStyle(
color: _AppColors.textPrimary,
fontSize: 15,
height: 1.6,
fontFamily: 'monospace',
letterSpacing: 0.5,
),
decoration: const InputDecoration(
hintText: 'ستظهر النتيجة هنا...',
hintStyle: TextStyle(
color: _AppColors.textSec,
fontSize: 14),
border: InputBorder.none,
isDense: true,
contentPadding: EdgeInsets.zero,
),
onTap: () {
if (_outputController.text.isNotEmpty) {
_outputController.selection =
TextSelection(
baseOffset: 0,
extentOffset:
_outputController.text.length,
);
}
},
),
),
),
// ─── Footer hint ─────────────────────────────────────
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.info_outline_rounded,
color: _AppColors.textSec, size: 13),
const SizedBox(width: 6),
const Text(
'البيانات مشفرة بالكامل ولا تُخزَّن على الخادم',
style: TextStyle(
color: _AppColors.textSec,
fontSize: 12,
),
),
],
),
],
),
),
),
),
),
],
),
),
],
),
);
}
}
// ─── Reusable Widgets ──────────────────────────────────────────────────────────
/// Glass card with optional custom header widget
class _GlassCard extends StatelessWidget {
final Widget child;
final Widget? headerWidget;
final Color borderColor;
const _GlassCard({
required this.child,
this.headerWidget,
this.borderColor = _AppColors.border,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: _AppColors.card,
borderRadius: BorderRadius.circular(22),
border: Border.all(color: borderColor, width: 1.2),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.35),
blurRadius: 24,
offset: const Offset(0, 8),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(22),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (headerWidget != null) headerWidget!,
if (headerWidget == null)
Padding(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 0),
child: child,
)
else
child,
if (headerWidget == null) const SizedBox(height: 20),
],
),
),
);
}
}
/// Gradient action button with glow
class _ActionButton extends StatefulWidget {
final String label;
final IconData icon;
final bool isLoading;
final VoidCallback? onPressed;
final Gradient gradient;
final Color glowColor;
final Color borderColor;
const _ActionButton({
required this.label,
required this.icon,
required this.isLoading,
required this.onPressed,
required this.gradient,
required this.glowColor,
required this.borderColor,
});
@override
State<_ActionButton> createState() => _ActionButtonState();
}
class _ActionButtonState extends State<_ActionButton>
with SingleTickerProviderStateMixin {
late final AnimationController _ctrl;
late final Animation<double> _scale;
@override
void initState() {
super.initState();
_ctrl = AnimationController(
vsync: this, duration: const Duration(milliseconds: 120));
_scale = Tween<double>(begin: 1.0, end: 0.95)
.animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeOut));
}
@override
void dispose() {
_ctrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (_) => _ctrl.forward(),
onTapUp: (_) => _ctrl.reverse(),
onTapCancel: () => _ctrl.reverse(),
onTap: widget.onPressed,
child: AnimatedBuilder(
animation: _scale,
builder: (_, child) =>
Transform.scale(scale: _scale.value, child: child),
child: AnimatedOpacity(
opacity: widget.onPressed == null ? 0.4 : 1.0,
duration: const Duration(milliseconds: 200),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
gradient: widget.gradient,
borderRadius: BorderRadius.circular(14),
boxShadow: [
BoxShadow(
color: widget.glowColor,
blurRadius: 16,
spreadRadius: 1,
offset: const Offset(0, 4),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: widget.isLoading
? [
const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: Colors.white, strokeWidth: 2),
),
]
: [
Icon(widget.icon, color: Colors.white, size: 18),
const SizedBox(width: 8),
Text(
widget.label,
style: const TextStyle(
color: Colors.white,
fontSize: 15,
fontWeight: FontWeight.w700,
letterSpacing: 0.3,
),
),
],
),
),
),
),
);
}
}
/// Small inline icon button (copy/paste)
class _MiniIconButton extends StatelessWidget {
final String label;
final IconData icon;
final Color color;
final VoidCallback onTap;
const _MiniIconButton({
required this.label,
required this.icon,
required this.color,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withOpacity(0.25)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: color, size: 14),
const SizedBox(width: 5),
Text(
label,
style: TextStyle(
color: color,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,366 @@
// ═══════════════════════════════════════════════════════════════
// fingerprint_migration.dart
// ───────────────────────────────────────────────────────────────
// أداة ترحيل البصمات القديمة للنظام الجديد
// ───────────────────────────────────────────────────────────────
// المشكلة:
// البصمة القديمة = encrypt(androidId_model_osVersion)
// البصمة الجديدة = encrypt(androidId_model)
//
// الحل:
// 1. نجيب كل البصمات من السيرفر (batch 50 في المرة)
// 2. نفك تشفير كل بصمة بـ EncryptionHelper
// 3. نحذف آخر جزء (osVersion) مع الـ _ قبله
// 4. نعيد التشفير
// 5. نرفع البصمة المحدّثة للسيرفر
//
// يُستخدم مرة واحدة فقط ثم يُحذف من التطبيق
// ═══════════════════════════════════════════════════════════════
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:siro_admin/controller/functions/encrypt_decrypt.dart' as X;
import '../../../constant/char_map.dart';
import '../../../constant/links.dart';
import '../../../controller/functions/crud.dart';
import '../../../controller/functions/encrypt_decrypt.dart';
import '../../../print.dart';
class FingerprintMigrationTool extends StatefulWidget {
const FingerprintMigrationTool({super.key});
@override
State<FingerprintMigrationTool> createState() =>
_FingerprintMigrationToolState();
}
class _FingerprintMigrationToolState extends State<FingerprintMigrationTool> {
// ── حالة الترحيل ──────────────────────────────────────────
bool _isRunning = false;
bool _isDone = false;
int _total = 0;
int _processed = 0;
int _updated = 0; // بصمات تم تحديثها
int _skipped = 0; // بصمات كانت بالفعل بالنظام الجديد
int _failed = 0; // فشل في المعالجة
String _currentLog = '';
static const int _batchSize = 50;
// ─────────────────────────────────────────────────────────────
// الدالة الرئيسية للترحيل
// ─────────────────────────────────────────────────────────────
Future<void> _startMigration() async {
setState(() {
_isRunning = true;
_isDone = false;
_processed = 0;
_updated = 0;
_skipped = 0;
_failed = 0;
_currentLog = 'جارٍ جلب البصمات من السيرفر...';
});
try {
// ── 1. جلب كل البصمات من السيرفر ──────────────────────
final allFingerprints = await _fetchAllFingerprints();
if (allFingerprints == null) {
_log('❌ فشل في جلب البيانات من السيرفر');
setState(() => _isRunning = false);
return;
}
_total = allFingerprints.length;
_log('✅ تم جلب $_total بصمة — بدء المعالجة...');
// ── 2. معالجة على batches ──────────────────────────────
for (int i = 0; i < allFingerprints.length; i += _batchSize) {
final batch = allFingerprints.skip(i).take(_batchSize).toList();
_log('⚙️ معالجة ${i + 1}${i + batch.length} من $_total');
// معالجة الـ batch بالتوازي
await Future.wait(
batch.map((record) => _processSingleRecord(record)),
);
// استراحة قصيرة بين الـ batches لحماية السيرفر
if (i + _batchSize < allFingerprints.length) {
await Future.delayed(const Duration(milliseconds: 300));
}
}
_log('🎉 اكتمل الترحيل!\n'
'محدَّث: $_updated | متجاوز: $_skipped | فاشل: $_failed');
setState(() {
_isDone = true;
_isRunning = false;
});
} catch (e) {
_log('❌ خطأ عام: $e');
setState(() => _isRunning = false);
}
}
// ─────────────────────────────────────────────────────────────
// جلب كل البصمات من السيرفر
// ─────────────────────────────────────────────────────────────
Future<List<Map<String, dynamic>>?> _fetchAllFingerprints() async {
try {
final response = await CRUD().post(
link: AppLink.getAllFingerprints, // أضفه في AppLink
payload: {
'admin_key': 'iuyweiruinakjbfkajkjlkmalkcxnlahd'
}, // مفتاح أمان للـ endpoint
);
if (response == 'failure' || response == null) return null;
final data = response['data'];
if (data is! List) return null;
return List<Map<String, dynamic>>.from(data);
} catch (e) {
Log.print('fetchAllFingerprints error: $e');
return null;
}
}
// ─────────────────────────────────────────────────────────────
// معالجة بصمة واحدة
// ─────────────────────────────────────────────────────────────
Future<void> _processSingleRecord(Map<String, dynamic> record) async {
final String passengerID = record['passengerID']?.toString() ?? '';
final String encryptedFp = record['fingerPrint']?.toString() ?? '';
final String userType = record['userType']?.toString() ?? 'passenger';
if (passengerID.isEmpty || encryptedFp.isEmpty) {
setState(() {
_failed++;
_processed++;
});
return;
}
try {
// ── فك التشفير ────────────────────────────────────────
final String rawFp = EncryptionHelper.instance.decryptData(encryptedFp);
// ── تحليل البصمة ──────────────────────────────────────
// الشكل القديم: "androidId_model_osVersion" (3 أجزاء أو أكثر)
// الشكل الجديد: "androidId_model" (جزءان فقط)
final List<String> parts = rawFp.split('_');
if (parts.length <= 2) {
// البصمة بالفعل بالنظام الجديد — تجاوزها
setState(() {
_skipped++;
_processed++;
});
return;
}
// ── حذف آخر جزء (osVersion) ──────────────────────────
// مثال: "abc123_SamsungA51_13" → "abc123_SamsungA51"
// نأخذ أول جزأين فقط بغض النظر عن عدد الأجزاء
final String newRawFp = '${parts[0]}_${parts[1]}';
// ── إعادة التشفير ─────────────────────────────────────
final String newEncryptedFp =
EncryptionHelper.instance.encryptData(newRawFp);
// ── رفع البصمة الجديدة للسيرفر ───────────────────────
final response = await CRUD().post(
link: AppLink.updateFingerprintAdmin, // أضفه في AppLink
payload: {
'passengerID': passengerID,
'fingerprint': newEncryptedFp,
'userType': userType,
'admin_key': 'iuyweiruinakjbfkajkjlkmalkcxnlahd',
},
);
if (response != 'failure' && response?['status'] == 'success') {
setState(() {
_updated++;
_processed++;
});
Log.print('✅ Updated: $passengerID | $rawFp$newRawFp');
} else {
setState(() {
_failed++;
_processed++;
});
Log.print('❌ Failed update: $passengerID');
}
} catch (e) {
// فشل فك التشفير أو إعادة التشفير
setState(() {
_failed++;
_processed++;
});
Log.print('❌ Process error for $passengerID: $e');
}
}
void _log(String message) {
Log.print(message);
setState(() => _currentLog = message);
}
// ─────────────────────────────────────────────────────────────
// UI
// ─────────────────────────────────────────────────────────────
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Fingerprint Migration Tool')),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ── شرح الأداة ──────────────────────────────────
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.orange.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.orange.shade200),
),
child: const Text(
'⚠️ هذه الأداة تُستخدم مرة واحدة فقط\n'
'تقوم بتحديث بصمات الأجهزة القديمة\n'
'لتكون متوافقة مع النظام الجديد (بدون OS version)',
style: TextStyle(fontSize: 14, height: 1.6),
),
),
Container(
child: TextButton(
onPressed: () {
print(EncryptionHelper.instance.decryptData('hbgbitbXrXrBr'));
},
child: Text(
"Decrypt Test",
),
),
),
Container(
child: TextButton(
onPressed: () {
print(EncryptionHelper.instance.encryptData(
'1B501143-C579-461C-B556-4E8B390EEFE1_iPhone'));
},
child: Text(
"Encrypt Test",
),
),
),
Container(
child: TextButton(
onPressed: () {
print(r('hbgbitbXrXrBr'));
},
child: Text(
"decrypt X.r",
),
),
),
const SizedBox(height: 24),
// ── شريط التقدم ─────────────────────────────────
if (_total > 0) ...[
Text('التقدم: $_processed / $_total'),
const SizedBox(height: 8),
LinearProgressIndicator(
value: _total > 0 ? _processed / _total : 0,
backgroundColor: Colors.grey.shade200,
color: _isDone ? Colors.green : Colors.blue,
),
const SizedBox(height: 16),
],
// ── إحصائيات ────────────────────────────────────
if (_processed > 0)
Row(children: [
_statChip('محدَّث', _updated, Colors.green),
const SizedBox(width: 8),
_statChip('متجاوز', _skipped, Colors.blue),
const SizedBox(width: 8),
_statChip('فاشل', _failed, Colors.red),
]),
const SizedBox(height: 16),
// ── السجل الحالي ─────────────────────────────────
if (_currentLog.isNotEmpty)
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Text(_currentLog,
style: const TextStyle(fontFamily: 'monospace')),
),
const Spacer(),
// ── زر التشغيل ──────────────────────────────────
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: (_isRunning || _isDone) ? null : _startMigration,
style: ElevatedButton.styleFrom(
backgroundColor: _isDone ? Colors.green : Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
child: _isRunning
? const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: Colors.white, strokeWidth: 2),
),
SizedBox(width: 12),
Text('جارٍ الترحيل...',
style: TextStyle(color: Colors.white)),
],
)
: Text(
_isDone ? '✅ اكتمل الترحيل' : 'بدء الترحيل',
style:
const TextStyle(color: Colors.white, fontSize: 16),
),
),
),
],
),
),
);
}
Widget _statChip(String label, int value, Color color) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: color.withOpacity(0.3)),
),
child: Text('$label: $value',
style: TextStyle(color: color, fontWeight: FontWeight.bold)),
);
}
}

View File

@@ -0,0 +1,659 @@
import 'package:flutter/material.dart';
import 'package:siro_admin/constant/colors.dart';
import 'package:siro_admin/constant/links.dart';
import 'package:siro_admin/controller/functions/crud.dart';
class ErrorLog {
final String id;
final String error;
final String userId;
final String userType;
final String phone;
final String createdAt;
final String device;
final String details;
final String status;
ErrorLog({
required this.id,
required this.error,
required this.userId,
required this.userType,
required this.phone,
required this.createdAt,
required this.device,
required this.details,
required this.status,
});
factory ErrorLog.fromJson(Map<String, dynamic> j) => ErrorLog(
id: (j['id'] ?? '').toString(),
error: (j['error'] ?? '').toString(),
userId: (j['userId'] ?? '').toString(),
userType: (j['userType'] ?? '').toString(),
phone: (j['phone'] ?? '').toString(),
createdAt: (j['created_at'] ?? '').toString(),
device: (j['device'] ?? '').toString(),
details: (j['details'] ?? '').toString(),
status: (j['status'] ?? '').toString(),
);
}
class ErrorListPage extends StatefulWidget {
const ErrorListPage({Key? key}) : super(key: key);
@override
State<ErrorListPage> createState() => _ErrorListPageState();
}
class _ErrorListPageState extends State<ErrorListPage> {
static String baseUrl = '${AppLink.server}/Admin/error';
static const String listEndpoint = "error_list_last20.php";
static const String searchEndpoint = "error_search_by_phone.php";
final TextEditingController _phoneCtrl = TextEditingController();
bool _loading = false;
String? _errorMsg;
List<ErrorLog> _items = [];
bool _searchMode = false;
@override
void initState() {
super.initState();
_fetchLast20();
}
Future<void> _fetchLast20() async {
setState(() {
_loading = true;
_errorMsg = null;
_searchMode = false;
});
try {
final res =
await CRUD().post(link: "$baseUrl/$listEndpoint", payload: {});
final map = (res);
if (map['status'] == 'success') {
final List data = (map['message'] ?? []) as List;
final items = data.map((e) => ErrorLog.fromJson(e)).toList();
setState(() {
_items = items.cast<ErrorLog>();
});
} else {
setState(() => _errorMsg = map['message']?.toString() ?? 'Failed');
}
} catch (e) {
setState(() => _errorMsg = e.toString());
} finally {
if (mounted) setState(() => _loading = false);
}
}
Future<void> _searchByPhone() async {
final phone = _phoneCtrl.text.trim();
if (phone.isEmpty) {
return _fetchLast20();
}
setState(() {
_loading = true;
_errorMsg = null;
_searchMode = true;
});
try {
final res = await CRUD()
.post(link: "$baseUrl/$searchEndpoint", payload: {"phone": phone});
final map = (res);
if (map['status'] == 'success') {
final List data = (map['message'] ?? []) as List;
final items = data.map((e) => ErrorLog.fromJson(e)).toList();
setState(() {
_items = items.cast<ErrorLog>();
});
} else {
setState(() => _errorMsg = map['message']?.toString() ?? 'Failed');
}
} catch (e) {
setState(() => _errorMsg = e.toString());
} finally {
if (mounted) setState(() => _loading = false);
}
}
void _clearSearch() {
_phoneCtrl.clear();
FocusScope.of(context).unfocus();
_fetchLast20();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF0F4F8),
appBar: _buildAppBar(),
body: Column(
children: [
_SearchBar(
controller: _phoneCtrl,
onSearch: _searchByPhone,
onClear: _clearSearch,
),
Expanded(
child: _buildBody(),
),
],
),
);
}
PreferredSizeWidget _buildAppBar() {
return AppBar(
elevation: 0,
backgroundColor: const Color(0xFF0F172A),
foregroundColor: Colors.white,
centerTitle: true,
title: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white.withOpacity(0.2)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline, color: Colors.red, size: 22),
const SizedBox(width: 8),
const Text(
"سجل الأخطاء",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
),
);
}
Widget _buildBody() {
if (_loading) {
return const Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF0F172A)),
),
);
}
if (_errorMsg != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 64, color: Colors.red.shade300),
const SizedBox(height: 16),
Text(
_errorMsg!,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.red.shade700,
fontSize: 14,
),
),
],
),
);
}
if (_items.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.check_circle_outline,
size: 64,
color: Colors.green.shade300,
),
const SizedBox(height: 16),
const Text(
'لا توجد سجلات أخطاء',
style: TextStyle(fontSize: 16, color: Colors.grey),
),
],
),
);
}
return RefreshIndicator(
onRefresh: () async {
if (_searchMode) {
await _searchByPhone();
} else {
await _fetchLast20();
}
},
color: const Color(0xFF0F172A),
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
itemCount: _items.length,
itemBuilder: (context, index) {
return _ErrorTile(_items[index], index);
},
),
);
}
@override
void dispose() {
_phoneCtrl.dispose();
super.dispose();
}
}
class _SearchBar extends StatefulWidget {
final TextEditingController controller;
final VoidCallback onSearch;
final VoidCallback onClear;
const _SearchBar({
required this.controller,
required this.onSearch,
required this.onClear,
});
@override
State<_SearchBar> createState() => _SearchBarState();
}
class _SearchBarState extends State<_SearchBar> {
bool _isFocused = false;
@override
Widget build(BuildContext context) {
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,
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 16,
spreadRadius: 2,
)
],
),
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Focus(
onFocusChange: (focused) {
setState(() => _isFocused = focused);
},
child: TextField(
controller: widget.controller,
keyboardType: TextInputType.phone,
textDirection: TextDirection.rtl,
onSubmitted: (_) => widget.onSearch(),
style: const TextStyle(fontSize: 15),
decoration: InputDecoration(
hintText: 'بحث برقم الهاتف',
hintStyle: TextStyle(color: Colors.grey.shade400),
prefixIcon: Icon(
Icons.search,
color: _isFocused
? const Color(0xFF0F172A)
: Colors.grey.shade400,
),
suffixIcon: widget.controller.text.isNotEmpty
? InkWell(
onTap: () {
widget.controller.clear();
setState(() {});
},
child: Icon(Icons.close,
color: Colors.grey.shade400, size: 20),
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide:
BorderSide(color: Colors.grey.shade300, width: 1),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide:
BorderSide(color: Colors.grey.shade300, width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: Color(0xFF0F172A),
width: 2,
),
),
filled: true,
fillColor: Colors.white,
contentPadding: const EdgeInsets.symmetric(vertical: 12),
),
onChanged: (_) => setState(() {}),
),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: widget.onSearch,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0F172A),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 0,
),
child: const Row(
children: [
Icon(Icons.search, size: 18),
SizedBox(width: 6),
Text("بحث"),
],
),
),
const SizedBox(width: 8),
OutlinedButton(
onPressed: widget.onClear,
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFF0F172A),
side: const BorderSide(
color: Color(0xFF0F172A),
width: 1.5,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(horizontal: 12),
),
child: const Icon(Icons.refresh, size: 20),
),
],
),
],
),
);
}
}
class _ErrorTile extends StatelessWidget {
final ErrorLog item;
final int index;
const _ErrorTile(this.item, this.index);
@override
Widget build(BuildContext context) {
// تحديد الألوان والأيقونات بناءً على نوع المستخدم
final isDriver = item.userType.toLowerCase().contains('driver') ||
item.userType.toLowerCase().contains('سائق');
final userTypeColor =
isDriver ? const Color(0xFF10B981) : const Color(0xFFF59E0B);
final userTypeIcon = isDriver ? Icons.directions_car : Icons.person;
final userTypeLabel = isDriver ? "سائق" : "راكب";
final userTypeBgColor = isDriver
? const Color(0xFF10B981).withOpacity(0.1)
: const Color(0xFFF59E0B).withOpacity(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,
),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey.shade200, width: 1),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.06),
blurRadius: 12,
spreadRadius: 1,
)
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Stack(
children: [
// خط علوي ملون
Positioned(
top: 0,
left: 0,
right: 0,
child: Container(
height: 4,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
const Color(0xFFEF4444),
Colors.red.shade400,
],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
),
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
// الصف الأول: رقم الخطأ ونوع المستخدم
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
decoration: BoxDecoration(
color: userTypeBgColor,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: userTypeColor, width: 1),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(userTypeIcon, size: 14, color: userTypeColor),
const SizedBox(width: 4),
Text(
userTypeLabel,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: userTypeColor,
),
),
],
),
),
Text(
'#${item.id}',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade500,
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 12),
// عنوان الخطأ (قابل للنسخ)
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Colors.red.shade200,
width: 1,
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.warning_amber_rounded,
size: 18, color: Colors.red.shade600),
const SizedBox(width: 8),
Expanded(
child: SelectableText(
item.error.isEmpty ? '(بدون عنوان)' : item.error,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: Colors.red.shade900,
height: 1.4,
),
),
),
],
),
),
const SizedBox(height: 12),
// التفاصيل (إن وجدت)
if (item.details.isNotEmpty) ...[
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Colors.grey.shade200,
width: 1,
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.info_outline,
size: 16, color: Colors.grey.shade600),
const SizedBox(width: 8),
Expanded(
child: SelectableText(
item.details,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade700,
height: 1.4,
),
),
),
],
),
),
const SizedBox(height: 12),
],
// معلومات تقنية
Wrap(
spacing: 6,
runSpacing: 6,
alignment: WrapAlignment.end,
children: [
_buildInfoBadge(
icon: Icons.phone,
label: 'الهاتف',
value: item.phone,
color: Colors.blue,
),
_buildInfoBadge(
icon: Icons.person_outline,
label: 'المعرف',
value: item.userId,
color: Colors.purple,
),
_buildInfoBadge(
icon: Icons.devices,
label: 'Path',
value: item.device,
color: Colors.orange,
),
_buildInfoBadge(
icon: Icons.schedule,
label: 'التاريخ',
value: item.createdAt,
color: Colors.teal,
),
],
),
],
),
),
],
),
),
);
}
Widget _buildInfoBadge({
required IconData icon,
required String label,
required String value,
required Color color,
}) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: color.withOpacity(0.08),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withOpacity(0.3), width: 1),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 13, color: color),
const SizedBox(width: 4),
Flexible(
child: Text.rich(
TextSpan(
children: [
TextSpan(
text: "$label: ",
style: TextStyle(
fontSize: 10,
color: color.withOpacity(0.7),
fontWeight: FontWeight.w500,
),
),
TextSpan(
text: value.isEmpty ? '' : value,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: color,
),
),
],
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,278 @@
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 {
const FinancialV2Page({super.key});
@override
Widget build(BuildContext context) {
final controller = Get.put(FinancialV2Controller());
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.fetchAllFinancials(),
)
],
),
body: GetBuilder<FinancialV2Controller>(
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: [
_buildMainFinancialStats(ctrl.stats),
const SizedBox(height: 24),
_buildSectionTitle('طرق الدفع'),
_buildPaymentMethodBreakdown(ctrl.stats),
const SizedBox(height: 32),
_buildSectionTitle('تسويات الكباتن (مستحقات معلقة)'),
_buildSettlementsList(ctrl.settlements),
const SizedBox(height: 40),
],
),
);
},
),
);
}
Widget _buildSectionTitle(String title) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Text(
title,
style: const TextStyle(
color: AppColor.textPrimary,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
);
}
Widget _buildMainFinancialStats(Map<String, dynamic> stats) {
return Column(
children: [
_buildFinancialCard(
'إجمالي عمولة المنصة',
'${stats['total_platform_commission'] ?? 0} ج.م',
Icons.account_balance_rounded,
AppColor.accent,
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildFinancialCard(
'إجمالي دخل الكباتن',
'${stats['total_driver_pay'] ?? 0}',
Icons.person_pin_rounded,
AppColor.info,
isSmall: true,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildFinancialCard(
'إجمالي الإيرادات',
'${stats['total_revenue'] ?? 0}',
Icons.payments_rounded,
AppColor.success,
isSmall: true,
),
),
],
),
],
);
}
Widget _buildFinancialCard(
String title, String value, IconData icon, Color color,
{bool isSmall = false}) {
return Container(
padding: EdgeInsets.all(isSmall ? 16 : 24),
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: color.withOpacity(0.2)),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [AppColor.surface, color.withOpacity(0.05)],
),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(14),
),
child: Icon(icon, color: color, size: isSmall ? 20 : 28),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: const TextStyle(
color: AppColor.textSecondary, fontSize: 12)),
const SizedBox(height: 4),
Text(value,
style: TextStyle(
color: AppColor.textPrimary,
fontSize: isSmall ? 18 : 24,
fontWeight: FontWeight.bold)),
],
),
),
],
),
);
}
Widget _buildPaymentMethodBreakdown(Map<String, dynamic> stats) {
double cash = double.tryParse(stats['cash_payments'].toString()) ?? 0;
double digital = double.tryParse(stats['digital_payments'].toString()) ?? 0;
double total = cash + digital;
if (total == 0) total = 1;
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(20),
),
child: Column(
children: [
_buildPaymentBar('نقدي (Cash)', cash, total, AppColor.warning),
const SizedBox(height: 16),
_buildPaymentBar('إلكتروني / محفظة', digital, total, AppColor.info),
],
),
);
}
Widget _buildPaymentBar(
String label, double value, double total, Color color) {
double percent = value / total;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label,
style:
const TextStyle(color: AppColor.textPrimary, fontSize: 13)),
Text('${value.toStringAsFixed(0)} ج.م',
style: TextStyle(color: color, fontWeight: FontWeight.bold)),
],
),
const SizedBox(height: 8),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: percent,
backgroundColor: AppColor.divider,
color: color,
minHeight: 8,
),
),
],
);
}
Widget _buildSettlementsList(List<dynamic> settlements) {
if (settlements.isEmpty)
return const Center(child: Text('لا توجد تسويات معلقة'));
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: settlements.length,
itemBuilder: (ctx, i) {
final s = settlements[i];
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColor.divider),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${s['first_name']} ${s['last_name']}',
style: const TextStyle(
color: AppColor.textPrimary,
fontWeight: FontWeight.bold)),
Text(s['phone'] ?? '',
style: const TextStyle(
color: AppColor.textSecondary, fontSize: 12)),
const SizedBox(height: 4),
Text('${s['total_rides']} رحلة مكتملة',
style: const TextStyle(
color: AppColor.info, fontSize: 11)),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
const Text('المستحقات',
style: TextStyle(
color: AppColor.textSecondary, fontSize: 10)),
Text('${s['total_earned']} ج.م',
style: const TextStyle(
color: AppColor.accent,
fontSize: 18,
fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
backgroundColor: AppColor.accent.withOpacity(0.1),
foregroundColor: AppColor.accent,
elevation: 0,
minimumSize: const Size(80, 32),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8)),
),
child: const Text('تسوية',
style: TextStyle(
fontSize: 11, fontWeight: FontWeight.bold)),
),
],
),
],
),
);
},
);
}
}

View File

@@ -0,0 +1,603 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'dart:convert';
import 'package:siro_admin/constant/links.dart';
import 'package:siro_admin/controller/functions/crud.dart';
import 'package:siro_admin/views/widgets/my_textField.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 _danger = Color(0xFFFF5370);
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});
final PackageController packageController = Get.put(PackageController());
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: _bg,
appBar: _buildAppBar(),
body: GetBuilder<PackageController>(
builder: (controller) {
if (controller.isLoading.value) {
return const Center(
child: CircularProgressIndicator(color: _accent, strokeWidth: 2),
);
}
if (controller.packages.isEmpty) {
return _buildEmptyState();
}
return ListView.separated(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 40),
itemCount: controller.packages.length,
separatorBuilder: (_, __) => const SizedBox(height: 10),
itemBuilder: (context, index) {
final package = controller.packages[index];
return _buildPackageCard(context, package, controller);
},
);
},
),
);
}
// ─────────────────────────── APP BAR ───────────────────────────
PreferredSizeWidget _buildAppBar() {
return AppBar(
backgroundColor: _bg,
elevation: 0,
surfaceTintColor: Colors.transparent,
leading: GestureDetector(
onTap: () => Get.back(),
child: Container(
margin: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: _surface,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: _divider),
),
child: const Icon(Icons.arrow_back_ios_new_rounded,
color: _textSecondary, size: 16),
),
),
title: Row(
children: [
Container(
padding: const EdgeInsets.all(7),
decoration: BoxDecoration(
color: _accent.withOpacity(0.12),
borderRadius: BorderRadius.circular(9),
border: Border.all(color: _accent.withOpacity(0.25)),
),
child: const Icon(Icons.system_update_rounded,
color: _accent, size: 16),
),
const SizedBox(width: 10),
const Text(
'تحديث التطبيق',
style: TextStyle(
color: _textPrimary,
fontSize: 17,
fontWeight: FontWeight.w600,
),
),
],
),
actions: [
GestureDetector(
onTap: () => packageController.fetchPackages(),
child: Container(
margin: const EdgeInsets.only(right: 16),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: _surface,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: _divider),
),
child: const Icon(Icons.refresh_rounded,
color: _textSecondary, size: 18),
),
),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1),
child: Container(height: 1, color: _divider),
),
);
}
// ─────────────────────────── PACKAGE CARD ───────────────────────────
Widget _buildPackageCard(
BuildContext context, dynamic package, PackageController controller) {
final platform = package['platform']?.toString() ?? '';
final isAndroid = platform.toLowerCase().contains('android');
final isIOS = platform.toLowerCase().contains('ios');
final Color platformColor = isAndroid
? const Color(0xFF4CAF50)
: isIOS
? _info
: _warning;
final IconData platformIcon = isAndroid
? Icons.android_rounded
: isIOS
? Icons.apple_rounded
: Icons.devices_rounded;
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _showUpdateDialog(context, package, controller),
borderRadius: BorderRadius.circular(16),
splashColor: _accent.withOpacity(0.06),
highlightColor: Colors.transparent,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: _surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: _divider),
),
child: Row(
children: [
// Platform Icon
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
platformColor.withOpacity(0.20),
platformColor.withOpacity(0.06),
],
),
borderRadius: BorderRadius.circular(13),
border: Border.all(color: platformColor.withOpacity(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,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Row(
children: [
_buildTag(platform, platformColor),
const SizedBox(width: 6),
_buildVersionBadge(
package['version']?.toString() ?? '?'),
],
),
],
),
),
// Update button
Container(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
decoration: BoxDecoration(
color: _accent.withOpacity(0.10),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: _accent.withOpacity(0.25)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: const [
Icon(Icons.edit_rounded, color: _accent, size: 13),
SizedBox(width: 5),
Text(
'تعديل',
style: TextStyle(
color: _accent,
fontSize: 11,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
),
),
);
}
Widget _buildTag(String label, Color color) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: color.withOpacity(0.10),
borderRadius: BorderRadius.circular(6),
),
child: Text(
label,
style: TextStyle(
color: color,
fontSize: 10,
fontWeight: FontWeight.w600,
),
),
);
}
Widget _buildVersionBadge(String version) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: _divider,
borderRadius: BorderRadius.circular(6),
),
child: Text(
'v$version',
style: const TextStyle(
color: _textSecondary,
fontSize: 10,
fontWeight: FontWeight.w500,
fontFamily: 'monospace',
),
),
);
}
// ─────────────────────────── EMPTY STATE ───────────────────────────
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: _surface,
shape: BoxShape.circle,
border: Border.all(color: _divider),
),
child: const Icon(Icons.inventory_2_outlined,
color: _textSecondary, size: 32),
),
const SizedBox(height: 16),
const Text('لا توجد حزم متاحة',
style: TextStyle(
color: _textPrimary,
fontSize: 15,
fontWeight: FontWeight.w600)),
const SizedBox(height: 6),
const Text('اسحب للأسفل لإعادة التحميل',
style: TextStyle(color: _textSecondary, fontSize: 12)),
],
),
);
}
// ─────────────────────────── UPDATE DIALOG ───────────────────────────
void _showUpdateDialog(
BuildContext context, dynamic package, PackageController controller) {
controller.versionController.clear();
Get.dialog(
Dialog(
backgroundColor: Colors.transparent,
child: Container(
decoration: BoxDecoration(
color: _surfaceElevated,
borderRadius: BorderRadius.circular(24),
border: Border.all(color: _divider),
boxShadow: const [
BoxShadow(
color: Colors.black54,
blurRadius: 30,
offset: Offset(0, 12),
),
],
),
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: _accent.withOpacity(0.12),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: _accent.withOpacity(0.25)),
),
child: const Icon(Icons.system_update_rounded,
color: _accent, size: 20),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'تحديث الإصدار',
style: TextStyle(
color: _textPrimary,
fontSize: 16,
fontWeight: FontWeight.w700,
),
),
Text(
package['appName']?.toString() ?? '',
style: const TextStyle(
color: _textSecondary, fontSize: 11),
),
],
),
),
],
),
const SizedBox(height: 20),
Container(height: 1, color: _divider),
const SizedBox(height: 20),
// Current info
Row(
children: [
_buildInfoChip(Icons.devices_rounded,
package['platform']?.toString() ?? '', _info),
const SizedBox(width: 8),
_buildInfoChip(Icons.tag_rounded,
'الحالي: ${package['version']}', _warning),
],
),
const SizedBox(height: 18),
// Input label
const Text(
'الإصدار الجديد',
style: TextStyle(
color: _textSecondary,
fontSize: 11,
fontWeight: FontWeight.w600,
letterSpacing: 0.8,
),
),
const SizedBox(height: 8),
// Text input
Container(
decoration: BoxDecoration(
color: _bg,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: _divider),
),
child: TextField(
controller: controller.versionController,
keyboardType:
const TextInputType.numberWithOptions(decimal: true),
style: const TextStyle(
color: _textPrimary,
fontSize: 15,
fontFamily: 'monospace',
fontWeight: FontWeight.w600,
),
decoration: InputDecoration(
hintText: package['version'].toString(),
hintStyle: const TextStyle(
color: _textSecondary,
fontFamily: 'monospace',
),
prefixIcon:
const Icon(Icons.tag_rounded, color: _accent, size: 18),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(
horizontal: 14, vertical: 14),
),
),
),
const SizedBox(height: 24),
// Actions
Row(
children: [
Expanded(
child: TextButton(
onPressed: () => Get.back(),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: const BorderSide(color: _divider),
),
),
child: const Text(
'إلغاء',
style: TextStyle(color: _textSecondary, fontSize: 13),
),
),
),
const SizedBox(width: 12),
Expanded(
child: Obx(() => ElevatedButton.icon(
icon: controller.isLoading.value
? const SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: const Icon(Icons.check_rounded, size: 16),
label: Text(
controller.isLoading.value ? 'جاري...' : 'تحديث',
style: const TextStyle(fontSize: 13),
),
style: ElevatedButton.styleFrom(
backgroundColor: _accent,
foregroundColor: _bg,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 0,
),
onPressed: controller.isLoading.value
? null
: () async {
await controller.updatePackages(
package['id'].toString(),
controller.versionController.text,
);
},
)),
),
],
),
],
),
),
),
);
}
Widget _buildInfoChip(IconData icon, String label, Color color) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: color.withOpacity(0.08),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withOpacity(0.2)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: color, size: 13),
const SizedBox(width: 5),
Text(
label,
style: TextStyle(
color: color,
fontSize: 11,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
}
// ══════════════════════════════════════════════════════════════
// CONTROLLER
// ══════════════════════════════════════════════════════════════
class PackageController extends GetxController {
List packages = [];
var isLoading = false.obs;
final versionController = TextEditingController();
final formKey = GlobalKey<FormState>();
@override
void onInit() {
super.onInit();
fetchPackages();
}
fetchPackages() async {
isLoading.value = true;
var response = await CRUD().get(link: AppLink.getPackages, payload: {});
if (response is String && (response == 'failure' || response == 'token_expired')) {
isLoading.value = false;
return;
}
try {
var jsonData = response is String ? jsonDecode(response) : response;
packages = jsonData['message'] ?? [];
Log.print('✅ Decoded packages: ${packages.length} items');
update();
} catch (e) {
Log.print('❌ Error parsing packages: $e');
}
isLoading.value = false;
}
updatePackages(String id, String version) async {
if (version.trim().isEmpty) {
Get.snackbar(
'تنبيه',
'يرجى إدخال رقم الإصدار',
backgroundColor: _warning.withOpacity(0.15),
colorText: _textPrimary,
borderRadius: 12,
margin: const EdgeInsets.all(16),
icon: const Icon(Icons.warning_rounded, color: _warning),
);
return;
}
isLoading.value = true;
var response = await CRUD().post(
link: AppLink.updatePackages,
payload: {"id": id, "version": version},
);
Log.print('response: $response');
isLoading.value = false;
if (response != 'failure') {
Get.back();
Get.snackbar(
'تم التحديث',
'تم تحديث الإصدار بنجاح',
backgroundColor: _accent.withOpacity(0.15),
colorText: _textPrimary,
borderRadius: 12,
margin: const EdgeInsets.all(16),
icon: const Icon(Icons.check_circle_rounded, color: _accent),
);
fetchPackages();
} else {
Get.snackbar(
'خطأ',
'فشل التحديث، يرجى المحاولة مجدداً',
backgroundColor: _danger.withOpacity(0.15),
colorText: _textPrimary,
borderRadius: 12,
margin: const EdgeInsets.all(16),
icon: const Icon(Icons.error_rounded, color: _danger),
);
}
}
}

View File

@@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../constant/colors.dart';
import '../../../constant/style.dart';
import '../../../controller/admin/passenger_admin_controller.dart';
import '../../widgets/elevated_btn.dart';
import 'passenger_details_page.dart';
GetBuilder<PassengerAdminController> formSearchPassengers() {
// DbSql sql = DbSql.instance;
return GetBuilder<PassengerAdminController>(
builder: (controller) => Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Container(
decoration:
const BoxDecoration(color: AppColor.secondaryColor),
child: TextField(
decoration: InputDecoration(
border: const OutlineInputBorder(
borderRadius: BorderRadius.only(),
gapPadding: 4,
borderSide: BorderSide(
color: AppColor.redColor,
width: 2,
)),
suffixIcon: InkWell(
onTap: () async {
if (controller.passengerController.text.length >
4) {
await controller.getPassengers();
Get.defaultDialog(
title: controller.passengers['message'][0]
['email'],
titleStyle: AppStyle.title,
content: Column(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
'Name is ${controller.passengers['message'][0]['first_name']} ${controller.passengers['message'][0]['last_name']}',
style: AppStyle.title,
),
Text(
'phone is ${controller.passengers['message'][0]['phone']}',
style: AppStyle.title,
),
],
),
confirm: MyElevatedButton(
title: 'Go To Details'.tr,
onPressed: () {
Get.to(
() => const PassengerDetailsPage(),
arguments: {
'data': controller
.passengers['message'][0],
});
}));
}
},
child: const Icon(Icons.search)),
hintText: 'Search for Passenger'.tr,
hintStyle: AppStyle.title,
hintMaxLines: 1,
prefixIcon: IconButton(
onPressed: () async {
controller.passengerController.clear();
controller.clearPlaces();
},
icon: Icon(
Icons.clear,
color: Colors.red[300],
),
),
),
controller: controller.passengerController,
),
),
)
],
));
}

View File

@@ -0,0 +1,390 @@
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 'passenger_details_page.dart';
class Passengrs extends StatelessWidget {
Passengrs({super.key});
final PassengerAdminController passengerAdminController =
Get.put(PassengerAdminController());
@override
Widget build(BuildContext context) {
// 1. منطق السوبر أدمن
String myPhone = box.read(BoxName.adminPhone).toString();
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(
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);
},
),
),
],
),
);
}
// --- بناء قائمة الركاب ---
Widget _buildPassengersList(
PassengerAdminController controller, bool isSuperAdmin) {
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])),
],
),
);
}
return ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 16),
physics: const BouncingScrollPhysics(),
itemCount: passengers.length,
separatorBuilder: (context, index) => const SizedBox(height: 12),
itemBuilder: (context, index) {
final user = passengers[index];
return _buildPassengerItem(user, isSuperAdmin);
},
);
}
// --- عنصر الراكب الواحد (Card) ---
Widget _buildPassengerItem(dynamic user, bool isSuperAdmin) {
String firstName = user['first_name'] ?? '';
String lastName = user['last_name'] ?? '';
String fullName = '$firstName $lastName'.trim();
if (fullName.isEmpty) fullName = 'Unknown User';
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),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(15),
onTap: () {
// الانتقال للتفاصيل مع تمرير صلاحية الأدمن
Get.to(
() => const PassengerDetailsPage(),
arguments: {'data': user, 'isSuperAdmin': isSuperAdmin},
);
},
child: Padding(
padding: const EdgeInsets.all(12.0),
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),
),
),
const SizedBox(width: 15),
// Info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
fullName,
style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 16),
),
const SizedBox(height: 4),
// Stats Row
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),
),
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]),
),
],
),
const SizedBox(height: 4),
// Phone Number (Masked logic)
Row(
children: [
Icon(Icons.phone_iphone,
size: 12, color: Colors.grey[400]),
const SizedBox(width: 4),
Text(
_formatPhoneNumber(
user['phone'].toString(), isSuperAdmin),
style: TextStyle(
fontSize: 12,
color: Colors.grey[500],
fontFamily: 'monospace'),
),
],
),
// Email (Show only if Super Admin)
if (isSuperAdmin && user['email'] != null) ...[
const SizedBox(height: 2),
Text(
user['email'],
style:
TextStyle(fontSize: 10, color: Colors.grey[400]),
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
]
],
),
),
// Arrow
Icon(Icons.arrow_forward_ios_rounded,
size: 16, color: Colors.grey[300]),
],
),
),
),
),
);
}
// --- دالة تنسيق الرقم (إظهار آخر 4 أرقام لغير الأدمن) ---
String _formatPhoneNumber(String phone, bool isSuperAdmin) {
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
}
// --- دالة إضافة الجوائز ---
void _showAddPrizeDialog(PassengerAdminController controller) {
// التحقق من يوم السبت
if (DateTime.now().weekday == DateTime.saturday) {
Get.defaultDialog(
title: 'Add Prize'.tr,
titleStyle: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
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]),
),
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',
onPressed: () async {
if (controller.formPrizeKey.currentState!.validate()) {
controller.addPassengerPrizeToWalletSecure();
Get.back();
}
},
),
),
cancel: TextButton(
onPressed: () => Get.back(),
child:
Text('Cancel'.tr, style: const TextStyle(color: Colors.grey))),
);
} else {
Get.snackbar(
'Not Allowed'.tr,
'Prizes can only be added on Saturdays.'.tr,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
icon: const Icon(Icons.error_outline, color: Colors.red),
snackPosition: SnackPosition.TOP,
margin: const EdgeInsets.all(10),
);
}
}
}

View File

@@ -0,0 +1,458 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../constant/box_name.dart';
import '../../../constant/colors.dart';
import '../../../controller/admin/passenger_admin_controller.dart';
import '../../../controller/functions/crud.dart';
import '../../../controller/firebase/firbase_messge.dart';
import '../../../constant/links.dart';
import '../../../main.dart'; // To access 'box' for admin phone check
import '../../widgets/elevated_btn.dart';
import '../../widgets/my_scafold.dart';
import '../../widgets/my_textField.dart';
import 'form_passenger.dart';
class PassengerDetailsPage extends StatelessWidget {
const PassengerDetailsPage({super.key});
@override
Widget build(BuildContext context) {
final Map<String, dynamic> data = Get.arguments['data'];
final controller = Get.find<PassengerAdminController>();
// 1. Define Super Admin Logic (Same as Captains Page)
String myPhone = box.read(BoxName.adminPhone).toString();
bool isSuperAdmin = myPhone == '963942542053' || myPhone == '963992952235';
return MyScafolld(
title: 'Passenger Profile'.tr,
isleading: true,
body: [
SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 40),
child: Column(
children: [
// --- Header Section (Avatar & Name) ---
_buildHeaderSection(context, data),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: [
// --- Personal Information Card ---
_buildInfoCard(
title: 'Personal Information',
icon: Icons.person,
children: [
_buildDetailTile(
Icons.email_outlined,
'Email',
isSuperAdmin
? data['email']
: _maskEmail(data['email']),
),
_buildDetailTile(
Icons.phone_iphone,
'Phone',
_formatPhoneNumber(
data['phone'].toString(), isSuperAdmin),
),
_buildDetailTile(
Icons.transgender,
'Gender',
data['gender'] ?? 'Not specified',
),
_buildDetailTile(
Icons.cake_outlined,
'Birthdate',
data['birthdate'] ?? 'N/A',
),
_buildDetailTile(
Icons.location_on_outlined,
'Site',
data['site'] ?? 'N/A',
),
// SOS Phone is critical, usually shown, but we can mask it too if needed
_buildDetailTile(
Icons.sos,
'SOS Phone',
data['sosPhone'] ?? 'N/A',
valueColor: Colors.redAccent,
),
],
),
const SizedBox(height: 16),
// --- Ride Statistics Card ---
_buildInfoCard(
title: 'Activity & Stats',
icon: Icons.bar_chart_rounded,
children: [
_buildDetailTile(
Icons.star_rate_rounded,
'Rating',
'${data['ratingPassenger'] ?? 0.0}',
valueColor: Colors.amber[700],
),
_buildDetailTile(
Icons.directions_car_filled_outlined,
'Total Rides',
data['countPassengerRide'],
),
_buildDetailTile(
Icons.cancel_outlined,
'Canceled Rides',
data['countPassengerCancel'],
valueColor: Colors.redAccent,
),
_buildDetailTile(
Icons.rate_review_outlined,
'Feedback Given',
data['countFeedback'],
),
],
),
const SizedBox(height: 30),
// --- Action Buttons ---
_buildActionButtons(
context, controller, data, isSuperAdmin),
],
),
),
],
),
),
],
);
}
// --- Header with Gradient/White Background ---
Widget _buildHeaderSection(BuildContext context, Map<String, dynamic> data) {
String firstName = data['first_name'] ?? '';
String lastName = data['last_name'] ?? '';
String fullName = '$firstName $lastName'.trim();
if (fullName.isEmpty) fullName = "Passenger";
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 25),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 5),
)
],
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(30)),
),
child: Column(
children: [
CircleAvatar(
radius: 45,
backgroundColor: AppColor.primaryColor.withOpacity(0.1),
child: Text(
fullName[0].toUpperCase(),
style: TextStyle(
fontSize: 35,
fontWeight: FontWeight.bold,
color: AppColor.primaryColor),
),
),
const SizedBox(height: 12),
Text(
fullName,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.black87),
),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
),
child: Text(
data['status'] ?? 'Active',
style: const TextStyle(
fontSize: 12,
color: Colors.blue,
fontWeight: FontWeight.w600),
),
),
],
),
);
}
Widget _buildInfoCard(
{required String title,
required IconData icon,
required List<Widget> children}) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.05),
spreadRadius: 2,
blurRadius: 10)
],
border: Border.all(color: Colors.grey.withOpacity(0.1)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, color: AppColor.primaryColor, size: 22),
const SizedBox(width: 10),
Text(title.tr,
style: const TextStyle(
fontSize: 17, fontWeight: FontWeight.bold)),
],
),
Divider(height: 25, color: Colors.grey.withOpacity(0.2)),
...children,
],
),
);
}
Widget _buildDetailTile(IconData icon, String label, dynamic value,
{Color? valueColor}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8)),
child: Icon(icon, color: Colors.grey[600], size: 18),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label.tr,
style: TextStyle(fontSize: 12, color: Colors.grey[500])),
Text(
value?.toString() ?? 'N/A',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: valueColor ?? Colors.black87),
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
);
}
Widget _buildActionButtons(
BuildContext context,
PassengerAdminController controller,
Map<String, dynamic> data,
bool isSuperAdmin) {
return Column(
children: [
// --- Send Notification (For All Admins) ---
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton.icon(
icon: const Icon(Icons.notifications_active_outlined,
color: Colors.white),
label: Text("Send Notification".tr,
style: const TextStyle(color: Colors.white, fontSize: 16)),
style: ElevatedButton.styleFrom(
backgroundColor: AppColor.primaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
onPressed: () => _showSendNotificationDialog(controller, data),
),
),
// --- Edit/Delete (Super Admin Only) ---
if (isSuperAdmin) ...[
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
icon: const Icon(Icons.edit_note_rounded, size: 20),
label: Text("Edit".tr),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: AppColor.yellowColor,
elevation: 0,
side: BorderSide(color: AppColor.yellowColor),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(vertical: 12),
),
onPressed: () {
// Get.to(() => const FormPassenger(), arguments: {
// 'isEditMode': true,
// 'passengerData': data,
// });
},
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton.icon(
icon: const Icon(Icons.delete_outline_rounded, size: 20),
label: Text("Delete".tr),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red[50],
foregroundColor: Colors.red,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(vertical: 12),
),
onPressed: () => _showDeleteConfirmation(data),
),
),
],
),
] else ...[
// Message for normal admins
const SizedBox(height: 15),
Text(
"Only Super Admins can edit or delete passengers.",
style: TextStyle(
color: Colors.grey[400],
fontSize: 12,
fontStyle: FontStyle.italic),
)
],
],
);
}
// --- Helper: Format Phone (Last 4 digits for normal admin) ---
String _formatPhoneNumber(String phone, bool isSuperAdmin) {
if (isSuperAdmin) return phone;
if (phone.length <= 4) return phone;
return '${'*' * (phone.length - 4)}${phone.substring(phone.length - 4)}';
}
// --- Helper: Mask Email ---
String _maskEmail(String? email) {
if (email == null || email.isEmpty) return 'N/A';
int atIndex = email.indexOf('@');
if (atIndex <= 1) return email; // Too short to mask
return '${email.substring(0, 2)}****${email.substring(atIndex)}';
}
void _showSendNotificationDialog(
PassengerAdminController controller, Map<String, dynamic> data) {
Get.defaultDialog(
title: 'Send Notification'.tr,
titleStyle: const TextStyle(fontWeight: FontWeight.bold),
content: Form(
key: controller.formPrizeKey,
child: Column(
children: [
MyTextForm(
controller: controller.titleNotify,
label: 'Title'.tr,
hint: 'Notification title'.tr,
type: TextInputType.text),
const SizedBox(height: 10),
MyTextForm(
controller: controller.bodyNotify,
label: 'Body'.tr,
hint: 'Message body'.tr,
type: TextInputType.text)
],
),
),
confirm: SizedBox(
width: 100,
child: MyElevatedButton(
title: 'Send',
onPressed: () {
// Validate form safely
if (controller.formPrizeKey.currentState?.validate() ?? false) {
FirebaseMessagesController().sendNotificationToAnyWithoutData(
controller.titleNotify.text,
controller.bodyNotify.text,
data['passengerToken'],
'order.wav');
Get.back();
Get.snackbar('Success', 'Notification sent successfully!',
backgroundColor: Colors.green.withOpacity(0.2));
}
},
),
),
cancel: TextButton(
onPressed: () => Get.back(),
child: Text('Cancel'.tr, style: const TextStyle(color: Colors.grey))),
);
}
void _showDeleteConfirmation(Map<String, dynamic> user) {
Get.defaultDialog(
title: 'Confirm Deletion'.tr,
titleStyle:
const TextStyle(color: Colors.redAccent, fontWeight: FontWeight.bold),
middleText:
'Are you sure you want to delete ${user['first_name']}? This action cannot be undone.'
.tr,
confirm: ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.redAccent),
onPressed: () async {
// 1. Close Dialog
Get.back();
// 2. Perform Delete Operation
var res = await CRUD().post(
link: AppLink.admin_delete_and_blacklist_passenger,
payload: {
'id': user['id'],
'phone': user['phone'],
'reason': 'Deleted by admin',
},
);
// 3. Handle Result
if (res['status'] == 'success') {
Get.back(); // Go back to list page
Get.snackbar('Deleted', 'Passenger removed successfully',
backgroundColor: Colors.red.withOpacity(0.2));
// Ideally, trigger a refresh on the controller here
// Get.find<PassengerAdminController>().getAll();
} else {
Get.snackbar('Error', res['message'] ?? 'Failed to delete',
backgroundColor: Colors.red.withOpacity(0.2));
}
},
child: Text('Delete'.tr, style: const TextStyle(color: Colors.white)),
),
cancel: TextButton(
onPressed: () => Get.back(),
child: Text('Cancel'.tr, style: const TextStyle(color: Colors.grey)),
),
);
}
}

View File

@@ -0,0 +1,254 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../constant/colors.dart';
import '../../../constant/style.dart';
import '../../../controller/admin/kazan_controller.dart';
import '../../widgets/my_scafold.dart';
import '../../widgets/elevated_btn.dart';
class KazanEditorPage extends StatelessWidget {
KazanEditorPage({super.key});
final KazanController controller = Get.put(KazanController());
@override
Widget build(BuildContext context) {
return MyScafolld(
title: 'تعديل أسعار كازان'.tr,
isleading: true,
body: [
Obx(() => controller.isLoading.value && controller.kazanData.isEmpty
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionHeader('النسب العامة'),
_buildKazanCard(),
const SizedBox(height: 24),
_buildSectionHeader('أسعار الفئات الإضافية'),
_buildPricesGrid(),
const SizedBox(height: 32),
MyElevatedButton(
title: 'حفظ جميع التعديلات',
icon: Icons.save_rounded,
onPressed: () => _handleSave(),
),
const SizedBox(height: 100),
],
),
)),
],
);
}
Widget _buildSectionHeader(String title) {
return Padding(
padding: const EdgeInsets.only(bottom: 12, left: 4),
child: Text(
title,
style: AppStyle.title.copyWith(color: AppColor.accent),
),
);
}
Widget _buildKazanCard() {
return Container(
padding: const EdgeInsets.all(20),
decoration: AppStyle.cardDecoration,
child: Column(
children: [
_buildSliderItem(
'نسبة كازان العامة',
'kazan',
'النسبة المئوية التي تقتطعها المنصة من كل رحلة',
Icons.percent_rounded,
),
const Divider(height: 32, color: AppColor.divider),
_buildPriceInputRow(
'سعر الوقود المرجعي',
'fuelPrice',
'السعر المستخدم في حسابات تعويض الوقود',
Icons.local_gas_station_rounded,
),
],
),
);
}
Widget _buildPriceInputRow(String title, String key, String desc, IconData icon) {
final TextEditingController textController = TextEditingController(
text: controller.kazanData[key]?.toString() ?? '0'
);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Icon(icon, size: 20, color: AppColor.accent),
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)),
],
),
),
Container(
width: 100,
height: 40,
decoration: BoxDecoration(
color: AppColor.surfaceElevated,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColor.divider),
),
child: TextField(
controller: textController,
keyboardType: TextInputType.number,
textAlign: TextAlign.center,
style: AppStyle.number.copyWith(fontSize: 16, color: AppColor.accent),
decoration: const InputDecoration(
border: InputBorder.none,
isDense: true,
contentPadding: EdgeInsets.symmetric(vertical: 10),
),
onChanged: (val) => controller.kazanData[key] = val,
),
),
const SizedBox(width: 8),
Text('ل.س', style: AppStyle.caption),
],
),
);
}
Widget _buildPricesGrid() {
final Map<String, dynamic> priceFields = {
'comfortPrice': {'label': 'Comfort', 'icon': Icons.chair_rounded},
'speedPrice': {'label': 'Speed', 'icon': Icons.flash_on_rounded},
'familyPrice': {'label': 'Family', 'icon': Icons.groups_rounded},
'deliveryPrice': {'label': 'Delivery', 'icon': Icons.delivery_dining_rounded},
'freePrice': {'label': 'Free', 'icon': Icons.money_off_rounded},
'latePrice': {'label': 'Late Night', 'icon': Icons.nightlight_round},
'heavyPrice': {'label': 'Heavy Load', 'icon': Icons.inventory_2_rounded},
'naturePrice': {'label': 'Nature', 'icon': Icons.forest_rounded},
};
return Container(
decoration: AppStyle.cardDecoration,
padding: const EdgeInsets.all(12),
child: GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 2.5,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: priceFields.length,
itemBuilder: (context, index) {
String key = priceFields.keys.elementAt(index);
var field = priceFields[key];
return _buildCompactPriceInputCard(key, field['label'], field['icon']);
},
),
);
}
Widget _buildCompactPriceInputCard(String key, String label, IconData icon) {
final TextEditingController textController = TextEditingController(
text: controller.kazanData[key]?.toString() ?? '0'
);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppColor.surfaceElevated,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColor.divider.withAlpha(100)),
),
child: Row(
children: [
Icon(icon, size: 16, color: AppColor.textSecondary),
const SizedBox(width: 6),
Expanded(
child: Text(label, style: AppStyle.caption.copyWith(fontSize: 11), overflow: TextOverflow.ellipsis),
),
SizedBox(
width: 50,
child: TextField(
controller: textController,
keyboardType: TextInputType.number,
textAlign: TextAlign.center,
style: AppStyle.number.copyWith(fontSize: 14),
decoration: const InputDecoration(
border: InputBorder.none,
isDense: true,
contentPadding: EdgeInsets.zero,
),
onChanged: (val) => controller.kazanData[key] = val,
),
),
],
),
);
}
Widget _buildSliderItem(String title, String key, String desc, IconData icon) {
double value = double.tryParse(controller.kazanData[key]?.toString() ?? '0') ?? 0;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(icon, size: 18, color: AppColor.accent),
const SizedBox(width: 8),
Text(title, style: AppStyle.title),
],
),
Text(
'${value.toInt()}%',
style: AppStyle.number.copyWith(fontSize: 18),
),
],
),
const SizedBox(height: 4),
Text(desc, style: AppStyle.caption),
Slider(
value: value.clamp(0, 100),
min: 0,
max: 100,
activeColor: AppColor.accent,
inactiveColor: AppColor.divider,
onChanged: (val) {
controller.kazanData[key] = val.toInt().toString();
},
),
],
);
}
void _handleSave() async {
final data = Map<String, dynamic>.from(controller.kazanData);
data['adminId'] = 'admin'; // Should be dynamic from auth service
data['country'] = 'syria';
bool success = await controller.updateKazan(data);
if (success) {
Get.snackbar("نجاح", "تم تحديث الأسعار بنجاح",
backgroundColor: AppColor.successSoft,
colorText: AppColor.textPrimary,
snackPosition: SnackPosition.BOTTOM,
margin: const EdgeInsets.all(16)
);
}
}
}

View File

@@ -0,0 +1,269 @@
import 'package:flutter/material.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';
class PromoManagementPage extends StatelessWidget {
PromoManagementPage({super.key});
final PromoController controller = Get.put(PromoController());
@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),
),
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);
},
),
)),
],
);
}
Widget _buildPromoCard(BuildContext context, dynamic promo) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
decoration: AppStyle.cardDecoration,
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
leading: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: AppColor.accentSoft,
borderRadius: BorderRadius.circular(12),
),
child: const Icon(Icons.local_offer_rounded, color: AppColor.accent),
),
title: Text(
promo['promo_code']?.toString() ?? 'N/A',
style: AppStyle.title,
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
Text(promo['description']?.toString() ?? '', style: AppStyle.caption),
const SizedBox(height: 4),
Row(
children: [
Icon(Icons.money_rounded, size: 14, color: AppColor.success),
const SizedBox(width: 4),
Text('% ${promo['amount']}', style: AppStyle.number.copyWith(color: AppColor.success)),
const SizedBox(width: 12),
Icon(Icons.person_rounded, size: 14, color: AppColor.info),
const SizedBox(width: 4),
Text(promo['passengerID'] == 'none' ? 'عام' : 'مخصص', style: AppStyle.caption),
],
),
],
),
trailing: PopupMenuButton(
icon: const Icon(Icons.more_vert_rounded, color: AppColor.textSecondary),
color: AppColor.surfaceElevated,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
itemBuilder: (context) => [
PopupMenuItem(
value: 'edit',
child: Row(
children: [
const Icon(Icons.edit_rounded, size: 18, color: AppColor.info),
const SizedBox(width: 8),
Text('تعديل'.tr),
],
),
),
PopupMenuItem(
value: 'delete',
child: Row(
children: [
const Icon(Icons.delete_outline_rounded, size: 18, color: AppColor.danger),
const SizedBox(width: 8),
Text('حذف'.tr, style: const TextStyle(color: AppColor.danger)),
],
),
),
],
onSelected: (value) {
if (value == 'edit') {
_showPromoSheet(context, promo: promo);
} else if (value == 'delete') {
MyDialog().getDialog(
'حذف كود الخصم',
'هل أنت متأكد من حذف كود الخصم ${promo['promo_code']}؟',
() => controller.deletePromo(promo['id'].toString()).then((_) => Get.back()),
);
}
},
),
),
);
}
void _showPromoSheet(BuildContext context, {dynamic promo}) {
final TextEditingController codeController = TextEditingController(text: promo?['promo_code']);
final TextEditingController amountController = TextEditingController(text: promo?['amount']?.toString());
final TextEditingController descController = TextEditingController(text: promo?['description']);
final TextEditingController passengerController = TextEditingController(text: promo?['passengerID'] ?? 'none');
final TextEditingController startDateController = TextEditingController(text: promo?['validity_start_date'] ?? DateTime.now().toString().split(' ')[0]);
final TextEditingController endDateController = TextEditingController(text: promo?['validity_end_date'] ?? DateTime.now().add(const Duration(days: 30)).toString().split(' ')[0]);
Get.bottomSheet(
Container(
padding: const EdgeInsets.all(24),
decoration: const BoxDecoration(
color: AppColor.surfaceElevated,
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40,
height: 4,
decoration: BoxDecoration(color: AppColor.divider, borderRadius: BorderRadius.circular(2)),
),
const SizedBox(height: 24),
Text(promo == null ? 'إضافة كود خصم جديد' : 'تعديل كود الخصم', style: AppStyle.headTitle),
const SizedBox(height: 24),
MyTextForm(
controller: codeController,
label: 'كود الخصم',
hint: 'مثال: WELCOME20',
type: TextInputType.text,
prefixIcon: Icons.local_offer_rounded,
),
MyTextForm(
controller: amountController,
label: 'نسبة الخصم',
hint: 'أدخل نسبة الخصم (مثال: 25)',
type: TextInputType.number,
prefixIcon: Icons.percent_rounded,
),
MyTextForm(
controller: descController,
label: 'الوصف',
hint: 'وصف كود الخصم',
type: TextInputType.text,
prefixIcon: Icons.description_rounded,
),
MyTextForm(
controller: passengerController,
label: 'معرف الراكب (أو none للعام)',
hint: 'none',
type: TextInputType.text,
prefixIcon: Icons.person_rounded,
),
Row(
children: [
Expanded(
child: InkWell(
onTap: () async {
DateTime? picked = await showDatePicker(
context: context,
initialDate: DateTime.tryParse(startDateController.text) ?? DateTime.now(),
firstDate: DateTime(2020),
lastDate: DateTime(2030),
);
if (picked != null) {
startDateController.text = picked.toString().split(' ')[0];
}
},
child: IgnorePointer(
child: MyTextForm(
controller: startDateController,
label: 'يبدأ في',
hint: 'YYYY-MM-DD',
type: TextInputType.none,
prefixIcon: Icons.calendar_today_rounded,
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: InkWell(
onTap: () async {
DateTime? picked = await showDatePicker(
context: context,
initialDate: DateTime.tryParse(endDateController.text) ?? DateTime.now(),
firstDate: DateTime(2020),
lastDate: DateTime(2030),
);
if (picked != null) {
endDateController.text = picked.toString().split(' ')[0];
}
},
child: IgnorePointer(
child: MyTextForm(
controller: endDateController,
label: 'ينتهي في',
hint: 'YYYY-MM-DD',
type: TextInputType.none,
prefixIcon: Icons.event_busy_rounded,
),
),
),
),
],
),
const SizedBox(height: 16),
MyElevatedButton(
title: promo == null ? 'إضافة' : 'حفظ التعديلات',
onPressed: () async {
final data = {
if (promo != null) 'id': promo['id'].toString(),
'promo_code': codeController.text,
'amount': amountController.text,
'description': descController.text,
'passengerID': passengerController.text,
'validity_start_date': startDateController.text,
'validity_end_date': endDateController.text,
};
bool success = promo == null
? await controller.addPromo(data)
: await controller.updatePromo(data);
if (success) Get.back();
},
),
const SizedBox(height: 16),
],
),
),
),
isScrollControlled: true,
);
}
}

View File

@@ -0,0 +1,153 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../controller/admin/quality_controller.dart';
class BlacklistPage extends StatelessWidget {
const BlacklistPage({super.key});
@override
Widget build(BuildContext context) {
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 TabBarView(
children: [
_buildDriverList(controller),
_buildPassengerList(controller),
],
);
},
),
),
);
}
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) {
Get.defaultDialog(
title: "تأكيد فك الحظر",
middleText:
"هل أنت متأكد من فك الحظر عن هذا ال$type ($identifier\nسيتم تسجيل هذه العملية في الـ Audit Log.",
textConfirm: "نعم، فك الحظر",
textCancel: "تراجع",
confirmTextColor: Colors.white,
buttonColor: Colors.green,
onConfirm: () {
Get.back();
onConfirm();
},
);
}
}

View File

@@ -0,0 +1,240 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../controller/admin/quality_controller.dart';
class DriverScorecardPage extends StatelessWidget {
final String driverId;
const DriverScorecardPage({super.key, required this.driverId});
@override
Widget build(BuildContext context) {
QualityController controller = Get.put(QualityController());
// Fetch data when page opens
WidgetsBinding.instance.addPostFrameCallback((_) {
controller.fetchDriverScorecard(driverId);
});
return Scaffold(
appBar: AppBar(
title: const Text('بطاقة أداء السائق',
style: TextStyle(fontWeight: FontWeight.bold)),
backgroundColor: Colors.blueAccent,
),
body: GetBuilder<QualityController>(
builder: (controller) {
if (controller.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (controller.scorecardData.isEmpty) {
return const Center(
child: Text('لا توجد بيانات لهذا السائق أو حدث خطأ.'));
}
var basicInfo = controller.scorecardData['basic_info'];
var ridesStats = controller.scorecardData['rides_stats'];
var rating = controller.scorecardData['rating'];
var behavior = controller.scorecardData['behavior'];
var complaints = controller.scorecardData['complaints'];
num overallScore = controller.scorecardData['overall_score'] ?? 0;
Color scoreColor = _getScoreColor(overallScore);
return SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// رأس البطاقة - معلومات السائق والتقييم الإجمالي
Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
CircleAvatar(
radius: 40,
backgroundColor: Colors.grey.shade300,
child: const Icon(Icons.person,
size: 50, color: Colors.white),
),
const SizedBox(height: 10),
Text(
'${basicInfo['first_name']} ${basicInfo['last_name']}',
style: const TextStyle(
fontSize: 22, fontWeight: FontWeight.bold)),
Text('هاتف: ${basicInfo['phone']}',
style: const TextStyle(color: Colors.grey)),
const Divider(height: 30),
Text('التقييم الشامل (Score)',
style: TextStyle(
fontSize: 18, color: Colors.grey.shade700)),
const SizedBox(height: 5),
Stack(
alignment: Alignment.center,
children: [
SizedBox(
width: 100,
height: 100,
child: CircularProgressIndicator(
value: overallScore / 100,
strokeWidth: 10,
backgroundColor: Colors.grey.shade200,
color: scoreColor,
),
),
Text('${overallScore.toStringAsFixed(1)}%',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: scoreColor)),
],
),
],
),
),
),
const SizedBox(height: 15),
// قسم إحصائيات الرحلات
_buildSectionTitle('إحصائيات الرحلات (الإنجاز)'),
Card(
elevation: 2,
child: ListTile(
leading: const Icon(Icons.drive_eta, color: Colors.blue),
title: const Text('نسبة الإنجاز'),
trailing: Text('${ridesStats['completion_rate']}%',
style: const TextStyle(
fontSize: 18, fontWeight: FontWeight.bold)),
subtitle: Text(
'إجمالي: ${ridesStats['total_rides']} | اكتمل: ${ridesStats['completed_rides']}\n'
'إلغاء سائق: ${ridesStats['driver_cancellations']} | إلغاء راكب: ${ridesStats['passenger_cancellations']}'),
),
),
const SizedBox(height: 10),
// قسم التقييم والشكاوى
_buildSectionTitle('رضا العملاء والشكاوى'),
Row(
children: [
Expanded(
child: Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
children: [
const Icon(Icons.star,
color: Colors.orange, size: 30),
const SizedBox(height: 5),
Text('${rating.toString()}/5.0',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold)),
const Text('متوسط التقييم',
style: TextStyle(
fontSize: 12, color: Colors.grey)),
],
),
),
),
),
Expanded(
child: Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
children: [
const Icon(Icons.warning,
color: Colors.redAccent, size: 30),
const SizedBox(height: 5),
Text('${complaints['total_complaints']} شكوى',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold)),
Text('${complaints['open_complaints']} مفتوحة',
style: const TextStyle(
fontSize: 12, color: Colors.red)),
],
),
),
),
),
],
),
const SizedBox(height: 10),
// قسم سلوك القيادة
_buildSectionTitle('سلوك القيادة والتتبع'),
Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
children: [
_buildBehaviorRow('متوسط سرعة السائق',
'${behavior['avg_max_speed']} كم/س', Icons.speed),
const Divider(),
_buildBehaviorRow(
'مرات الفرملة القاسية',
'${behavior['total_hard_brakes']}',
Icons.dangerous),
const Divider(),
_buildBehaviorRow(
'تسارع مفاجئ',
'${behavior['total_rapid_accel']}',
Icons.fast_forward),
const Divider(),
_buildBehaviorRow('تقييم السلوك الآلي',
'${behavior['avg_behavior_score']}%', Icons.memory),
],
),
),
),
],
),
);
},
),
);
}
Widget _buildSectionTitle(String title) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 4.0),
child: Text(title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black87)),
);
}
Widget _buildBehaviorRow(String title, String value, IconData icon) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(icon, size: 20, color: Colors.grey.shade600),
const SizedBox(width: 8),
Text(title, style: const TextStyle(fontSize: 15)),
],
),
Text(value,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
],
);
}
Color _getScoreColor(num score) {
if (score >= 80) return Colors.green;
if (score >= 60) return Colors.orange;
return Colors.red;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,236 @@
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});
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
// ),
// ],
// ),
// ),
// ),
// 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),
// ),
// ],
// ),
// ),
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,
),
),
),
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,
),
],
),
),
)
],
))
]);
}
}

View File

@@ -0,0 +1,121 @@
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';
import 'package:intl/intl.dart';
class AuditLogsPage extends StatelessWidget {
const AuditLogsPage({super.key});
@override
Widget build(BuildContext context) {
final controller = Get.put(SecurityV2Controller());
return Scaffold(
backgroundColor: AppColor.bg,
appBar: AppBar(
title: const Text('سجل العمليات (Audit Logs)',
style: TextStyle(fontWeight: FontWeight.bold)),
backgroundColor: AppColor.surface,
elevation: 0,
centerTitle: true,
),
body: GetBuilder<SecurityV2Controller>(
builder: (ctrl) {
if (ctrl.isLoading) {
return const Center(
child: CircularProgressIndicator(color: AppColor.accent));
}
if (ctrl.auditLogs.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.security_rounded,
size: 64, color: AppColor.textSecondary.withOpacity(0.3)),
const SizedBox(height: 16),
const Text('لا توجد عمليات مسجلة حالياً',
style: TextStyle(color: AppColor.textSecondary)),
const SizedBox(height: 8),
const Text(
'تأكد من إنشاء جدول سجل العمليات في قاعدة البيانات',
style: TextStyle(
color: AppColor.textSecondary, fontSize: 10)),
],
),
);
}
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: ctrl.auditLogs.length,
itemBuilder: (ctx, i) {
final log = ctrl.auditLogs[i];
return _buildLogItem(log);
},
);
},
),
);
}
Widget _buildLogItem(Map<String, dynamic> log) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColor.divider),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(log['admin_name'] ?? 'أدمن غير معروف',
style: const TextStyle(
color: AppColor.accent, fontWeight: FontWeight.bold)),
Text(log['created_at'] ?? '',
style: const TextStyle(
color: AppColor.textSecondary, fontSize: 11)),
],
),
const SizedBox(height: 8),
Text(log['action'] ?? '',
style: const TextStyle(
color: AppColor.textPrimary, fontWeight: FontWeight.w600)),
if (log['details'] != null) ...[
const SizedBox(height: 4),
Text(log['details'],
style: const TextStyle(
color: AppColor.textSecondary, fontSize: 12)),
],
const SizedBox(height: 8),
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppColor.info.withOpacity(0.1),
borderRadius: BorderRadius.circular(6),
),
child: Text(log['table_name'] ?? '',
style: const TextStyle(
color: AppColor.info,
fontSize: 10,
fontWeight: FontWeight.bold)),
),
const SizedBox(width: 8),
Text('ID: ${log['record_id']}',
style: const TextStyle(
color: AppColor.textSecondary, fontSize: 10)),
],
),
],
),
);
}
}

View File

@@ -0,0 +1,669 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../controller/server/server_monitor_controller.dart';
class ServerMonitorPage extends StatelessWidget {
const ServerMonitorPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final controller = Get.put(ServerMonitorController());
final themeColor = const Color(0xFF6366F1);
return Scaffold(
backgroundColor: const Color(0xFF0A0E27),
body: RefreshIndicator(
onRefresh: controller.fetchServerData,
color: themeColor,
backgroundColor: const Color(0xFF1A1F3A),
child: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: [
// === 1. App Bar المتجاوب ===
SliverAppBar(
expandedHeight: 100,
floating: true,
pinned: true,
backgroundColor: const Color(0xFF0A0E27),
elevation: 0,
flexibleSpace: FlexibleSpaceBar(
titlePadding: const EdgeInsets.only(bottom: 16),
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.dns_rounded,
color: Colors.white, size: 20),
const SizedBox(width: 8),
const Text(
'Server Monitor',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
color: Colors.white,
fontFamily: 'Segoe UI', // أو أي خط تفضله
),
),
],
),
centerTitle: true,
background: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
themeColor.withOpacity(0.3),
const Color(0xFF0A0E27),
],
),
),
),
),
actions: [
_buildRefreshButton(controller),
],
),
// === 2. المحتوى الرئيسي ===
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
sliver: Obx(() {
if (controller.isLoading.value &&
controller.serverData.value == null) {
return const SliverFillRemaining(child: _LoadingState());
}
if (controller.errorMessage.isNotEmpty) {
return SliverFillRemaining(
child: _ErrorState(controller: controller));
}
final data = controller.serverData.value!;
return SliverToBoxAdapter(
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth:
1000), // لمنع التمدد الزائد في الشاشات الكبيرة
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// معلومات الوقت والتشغيل
_HeaderInfo(data: data),
const SizedBox(height: 20),
// بطاقات الأداء (CPU & RAM)
LayoutBuilder(builder: (context, constraints) {
return _buildCpuMemSection(
data, constraints.maxWidth > 600);
}),
const SizedBox(height: 20),
// القسم المتغير (خدمات + عمليات + تخزين)
LayoutBuilder(builder: (context, constraints) {
// إذا كانت الشاشة كبيرة (تابلت/ديسكتوب)
if (constraints.maxWidth > 800) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// العمود الأول: الخدمات والشبكة
Expanded(
flex: 4,
child: Column(
children: [
_ServicesCard(data: data),
const SizedBox(height: 20),
_StorageNetworkCard(data: data),
],
),
),
const SizedBox(width: 20),
// العمود الثاني: العمليات
Expanded(
flex: 6,
child: _TopProcessesCard(
data: data,
height:
600), // ارتفاع ثابت في وضع الكمبيوتر
),
],
);
}
// إذا كانت الشاشة موبايل
else {
return Column(
children: [
_ServicesCard(data: data),
const SizedBox(height: 16),
_StorageNetworkCard(data: data),
const SizedBox(height: 16),
_TopProcessesCard(
data: data), // ارتفاع ديناميكي
],
);
}
}),
const SizedBox(height: 40),
],
),
),
),
);
}),
),
],
),
),
);
}
Widget _buildRefreshButton(ServerMonitorController controller) {
return Obx(() => Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: IconButton(
icon: controller.isLoading.value
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2, color: Colors.white),
)
: const Icon(Icons.refresh_rounded, color: Colors.white),
onPressed: controller.fetchServerData,
),
));
}
// دمج بطاقات المعالج والذاكرة
Widget _buildCpuMemSection(dynamic data, bool isWide) {
List<Widget> cards = [
_MetricCard(
title: "المعالج (CPU)",
value: "${data.cpu.percent}%",
subtitle: "${data.cpu.cores} Cores",
icon: Icons.memory,
percent: data.cpu.percent.toDouble(),
color: const Color(0xFFFF6B6B),
),
SizedBox(width: isWide ? 20 : 0, height: isWide ? 0 : 16),
_MetricCard(
title: "الذاكرة (RAM)",
value: "${data.memory.percent}%",
subtitle: "${data.memory.usedGb}/${data.memory.totalGb} GB",
icon: Icons.sd_storage_rounded,
percent: data.memory.percent.toDouble(),
color: const Color(0xFF4E54C8),
),
];
return isWide
? Row(
children: cards
.map((e) => e is SizedBox ? e : Expanded(child: e))
.toList())
: Column(children: cards);
}
}
// === مكونات فرعية معاد استخدامها (Widgets) ===
class _HeaderInfo extends StatelessWidget {
final dynamic data;
const _HeaderInfo({required this.data});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.05),
borderRadius: BorderRadius.circular(50),
border: Border.all(color: Colors.white.withOpacity(0.1)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.timer_outlined,
size: 16, color: Colors.greenAccent.withOpacity(0.8)),
const SizedBox(width: 8),
Text(
"Uptime: ${data.uptime.formatted}",
style: const TextStyle(
color: Colors.white70,
fontSize: 12,
fontWeight: FontWeight.w500),
),
Container(
width: 1,
height: 12,
color: Colors.white24,
margin: const EdgeInsets.symmetric(horizontal: 12)),
Icon(Icons.update,
size: 16, color: Colors.blueAccent.withOpacity(0.8)),
const SizedBox(width: 8),
Text(
"Last Update: ${data.timestamp.split(' ')[1]}",
style: const TextStyle(color: Colors.white70, fontSize: 12),
),
],
),
);
}
}
class _MetricCard extends StatelessWidget {
final String title;
final String value;
final String subtitle;
final IconData icon;
final double percent;
final Color color;
const _MetricCard({
required this.title,
required this.value,
required this.subtitle,
required this.icon,
required this.percent,
required this.color,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [color.withOpacity(0.9), color.withOpacity(0.6)],
),
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: color.withOpacity(0.3),
blurRadius: 12,
offset: const Offset(0, 8),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: Colors.white, size: 24),
),
Text(
value,
style: const TextStyle(
color: Colors.white,
fontSize: 28,
fontWeight: FontWeight.bold),
),
],
),
const SizedBox(height: 16),
Text(title,
style: const TextStyle(color: Colors.white70, fontSize: 14)),
const SizedBox(height: 4),
Text(subtitle,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600)),
const SizedBox(height: 12),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: percent / 100,
minHeight: 6,
backgroundColor: Colors.black12,
valueColor: const AlwaysStoppedAnimation(Colors.white),
),
),
],
),
);
}
}
class _ServicesCard extends StatelessWidget {
final dynamic data;
const _ServicesCard({required this.data});
@override
Widget build(BuildContext context) {
return _BaseCard(
title: "حالة الخدمات",
icon: Icons.security,
iconColor: Colors.tealAccent,
child: Column(
children: data.services.entries.map<Widget>((e) {
final isActive = e.value == 'active';
return Container(
margin: const EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF0F1629),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isActive
? Colors.green.withOpacity(0.3)
: Colors.red.withOpacity(0.3),
),
),
child: Row(
children: [
CircleAvatar(
radius: 4,
backgroundColor:
isActive ? Colors.greenAccent : Colors.redAccent,
),
const SizedBox(width: 12),
Expanded(
child: Text(
e.key.toUpperCase(),
style: const TextStyle(
color: Colors.white, fontWeight: FontWeight.w600),
),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: isActive
? Colors.green.withOpacity(0.1)
: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(6),
),
child: Text(
isActive ? "Running" : "Stopped",
style: TextStyle(
color: isActive ? Colors.greenAccent : Colors.redAccent,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
],
),
);
}).toList(),
),
);
}
}
class _StorageNetworkCard extends StatelessWidget {
final dynamic data;
const _StorageNetworkCard({required this.data});
@override
Widget build(BuildContext context) {
return _BaseCard(
title: "التخزين والشبكة",
icon: Icons.cloud_queue_rounded,
iconColor: Colors.purpleAccent,
child: Column(
children: [
_buildRowItem(Icons.pie_chart_outline, "Storage",
"${data.disk.percent}%", "${data.disk.usedGb} GB Used"),
const Divider(color: Colors.white10, height: 24),
_buildRowItem(Icons.download_rounded, "Download",
"${data.network.receivedMb} MB", "In"),
const SizedBox(height: 16),
_buildRowItem(Icons.upload_rounded, "Upload",
"${data.network.sentMb} MB", "Out"),
],
),
);
}
Widget _buildRowItem(IconData icon, String label, String value, String sub) {
return Row(
children: [
Icon(icon, color: Colors.white54, size: 20),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label,
style: const TextStyle(color: Colors.white60, fontSize: 12)),
Text(sub,
style: TextStyle(
color: Colors.white.withOpacity(0.4), fontSize: 10)),
],
),
const Spacer(),
Text(value,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16)),
],
);
}
}
class _TopProcessesCard extends StatelessWidget {
final dynamic data;
final double? height;
const _TopProcessesCard({required this.data, this.height});
@override
Widget build(BuildContext context) {
return Container(
height: height, // إذا كان null سيأخذ الارتفاع بناءً على المحتوى
decoration: BoxDecoration(
color: const Color(0xFF1A1F3A),
borderRadius: BorderRadius.circular(24),
border: Border.all(color: Colors.white.withOpacity(0.05)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(20),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.1),
borderRadius: BorderRadius.circular(8)),
child: const Icon(Icons.analytics_rounded,
color: Colors.orange, size: 18),
),
const SizedBox(width: 12),
const Text("Top Processes",
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold)),
],
),
),
const Divider(height: 1, color: Colors.white10),
// نستخدم ListView.builder داخل Expanded إذا كان هناك ارتفاع محدد، وإلا Column للموبايل
height != null
? Expanded(child: _buildList())
: _buildList(shrinkWrap: true),
],
),
);
}
Widget _buildList({bool shrinkWrap = false}) {
return ListView.separated(
padding: const EdgeInsets.all(16),
physics: shrinkWrap
? const NeverScrollableScrollPhysics()
: const BouncingScrollPhysics(),
shrinkWrap: shrinkWrap,
itemCount: data.topProcesses.length,
separatorBuilder: (_, __) => const SizedBox(height: 12),
itemBuilder: (context, index) {
final process = data.topProcesses[index];
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.03),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Text("#${index + 1}",
style: const TextStyle(
color: Colors.white38, fontWeight: FontWeight.bold)),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(process.name,
style: const TextStyle(
color: Colors.white, fontWeight: FontWeight.w500),
overflow: TextOverflow.ellipsis),
],
),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
),
child: Text(
process.usage,
style: const TextStyle(
color: Colors.orangeAccent,
fontSize: 12,
fontWeight: FontWeight.bold),
),
),
],
),
);
},
);
}
}
class _BaseCard extends StatelessWidget {
final String title;
final IconData icon;
final Color iconColor;
final Widget child;
const _BaseCard({
required this.title,
required this.icon,
required this.iconColor,
required this.child,
});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFF1A1F3A),
borderRadius: BorderRadius.circular(24),
border: Border.all(color: Colors.white.withOpacity(0.05)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: iconColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8)),
child: Icon(icon, color: iconColor, size: 18),
),
const SizedBox(width: 12),
Text(title,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold)),
],
),
const SizedBox(height: 20),
child,
],
),
);
}
}
class _LoadingState extends StatelessWidget {
const _LoadingState();
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(color: Color(0xFF6366F1)),
const SizedBox(height: 16),
Text("Connecting to server...",
style: TextStyle(color: Colors.white.withOpacity(0.5))),
],
),
);
}
}
class _ErrorState extends StatelessWidget {
final ServerMonitorController controller;
const _ErrorState({required this.controller});
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.cloud_off_rounded,
size: 60, color: Colors.redAccent),
const SizedBox(height: 16),
Text(controller.errorMessage.value,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white)),
const SizedBox(height: 24),
ElevatedButton(
onPressed: controller.fetchServerData,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF6366F1),
shape: const StadiumBorder(),
),
child: const Text("Try Again"),
)
],
),
),
);
}
}

View File

@@ -0,0 +1,195 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../controller/admin/staff_controller.dart';
class AddStaffPage extends StatelessWidget {
final String role; // 'admin' or 'service'
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);
return Scaffold(
backgroundColor: bgColor,
appBar: AppBar(
title: Text(role == 'admin' ? "إضافة مدير جديد" : "إضافة موظف خدمة عملاء"),
backgroundColor: bgColor,
elevation: 0,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Form(
key: controller.formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionTitle("المعلومات الأساسية"),
const SizedBox(height: 16),
_buildTextField(
controller: controller.nameController,
label: "الاسم الكامل",
icon: Icons.person_outline,
fillColor: inputColor,
),
const SizedBox(height: 16),
_buildTextField(
controller: controller.phoneController,
label: "رقم الهاتف",
icon: Icons.phone_android_outlined,
fillColor: inputColor,
keyboardType: TextInputType.phone,
),
const SizedBox(height: 16),
_buildTextField(
controller: controller.emailController,
label: "البريد الإلكتروني",
icon: Icons.email_outlined,
fillColor: inputColor,
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 16),
_buildTextField(
controller: controller.passwordController,
label: "كلمة المرور",
icon: Icons.lock_outline,
fillColor: inputColor,
obscureText: true,
),
const SizedBox(height: 24),
_buildSectionTitle("معلومات إضافية"),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildDropdown(
label: "الجنس",
value: controller.selectedGender,
items: ['Male', 'Female'],
onChanged: (val) => controller.selectedGender = val!,
fillColor: inputColor,
),
),
const SizedBox(width: 16),
Expanded(
child: _buildTextField(
controller: controller.birthdateController,
label: "تاريخ الميلاد",
icon: Icons.calendar_today_outlined,
fillColor: inputColor,
hint: "YYYY-MM-DD",
),
),
],
),
const SizedBox(height: 40),
GetBuilder<StaffController>(
builder: (controller) => SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton(
onPressed: controller.isLoading ? null : () => controller.registerStaff(),
style: ElevatedButton.styleFrom(
backgroundColor: accentColor,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
child: controller.isLoading
? const CircularProgressIndicator(color: Colors.white)
: Text(
"حفظ البيانات",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: bgColor,
),
),
),
),
),
],
),
),
),
);
}
Widget _buildSectionTitle(String title) {
return Text(
title,
style: const TextStyle(
color: Color(0xFF7D8590),
fontSize: 13,
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
),
);
}
Widget _buildTextField({
required TextEditingController controller,
required String label,
required IconData icon,
required Color fillColor,
String? hint,
bool obscureText = false,
TextInputType keyboardType = TextInputType.text,
}) {
return Container(
decoration: BoxDecoration(
color: fillColor,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white.withOpacity(0.05)),
),
child: TextFormField(
controller: controller,
obscureText: obscureText,
keyboardType: keyboardType,
style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
labelText: label,
hintText: hint,
hintStyle: const TextStyle(color: Colors.white24),
labelStyle: const TextStyle(color: Colors.white54),
prefixIcon: Icon(icon, color: Colors.white38),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
),
validator: (val) => val == null || val.isEmpty ? "هذا الحقل مطلوب" : null,
),
);
}
Widget _buildDropdown({
required String label,
required String value,
required List<String> items,
required Function(String?) onChanged,
required Color fillColor,
}) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: fillColor,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white.withOpacity(0.05)),
),
child: DropdownButtonFormField<String>(
value: value,
dropdownColor: fillColor,
style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
labelText: label,
labelStyle: const TextStyle(color: Colors.white54),
border: InputBorder.none,
),
items: items.map((e) => DropdownMenuItem(value: e, child: Text(e))).toList(),
onChanged: onChanged,
),
);
}
}

View File

@@ -0,0 +1,148 @@
import 'package:flutter/material.dart';
import '../../../constant/links.dart';
import '../../../controller/functions/crud.dart';
import '../../widgets/snackbar.dart';
class PendingAdminsPage extends StatefulWidget {
const PendingAdminsPage({super.key});
@override
State<PendingAdminsPage> createState() => _PendingAdminsPageState();
}
class _PendingAdminsPageState extends State<PendingAdminsPage> {
final CRUD _crud = CRUD();
bool _isLoading = true;
List _pendingAdmins = [];
@override
void initState() {
super.initState();
_fetchPendingAdmins();
}
Future<void> _fetchPendingAdmins() async {
setState(() => _isLoading = true);
try {
final response = await _crud.post(
link: '${AppLink.server}/Admin/auth/list_pending.php',
);
if (response != 'failure') {
setState(() {
_pendingAdmins = response['message'] ?? [];
});
}
} catch (e) {
mySnackeBarError('فشل في جلب البيانات: $e');
} finally {
setState(() => _isLoading = false);
}
}
Future<void> _handleAction(String adminId, String action) async {
try {
final response = await _crud.post(
link: '${AppLink.server}/Admin/auth/approve_admin.php',
payload: {
'admin_id': adminId,
'action': action,
},
);
if (response != 'failure') {
mySnackbarSuccess('تم تنفيذ الإجراء بنجاح');
_fetchPendingAdmins(); // تحديث القائمة
}
} catch (e) {
mySnackeBarError('حدث خطأ: $e');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0A0D14),
appBar: AppBar(
title: const Text('طلبات الانضمام المعلقة', style: TextStyle(fontWeight: FontWeight.bold)),
backgroundColor: const Color(0xFF161D2E),
elevation: 0,
),
body: _isLoading
? const Center(child: CircularProgressIndicator(color: Color(0xFF00E5FF)))
: _pendingAdmins.isEmpty
? _buildEmptyState()
: _buildList(),
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.person_search_rounded, size: 80, color: Colors.grey[800]),
const SizedBox(height: 16),
const Text('لا توجد طلبات معلقة حالياً', style: TextStyle(color: Colors.grey)),
],
),
);
}
Widget _buildList() {
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _pendingAdmins.length,
itemBuilder: (context, index) {
final admin = _pendingAdmins[index];
return Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF161D2E),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFF1F2D4A)),
),
child: Column(
children: [
ListTile(
contentPadding: EdgeInsets.zero,
leading: const CircleAvatar(
backgroundColor: Color(0xFF1F2D4A),
child: Icon(Icons.person, color: Color(0xFF00E5FF)),
),
title: Text(admin['name'] ?? 'بدون اسم', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
subtitle: Text(admin['phone'] ?? 'بدون رقم', style: const TextStyle(color: Colors.grey)),
trailing: Text(
admin['created_at']?.split(' ')[0] ?? '',
style: const TextStyle(color: Colors.grey, fontSize: 12),
),
),
const Divider(color: Color(0xFF1F2D4A), height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => _handleAction(admin['id'], 'rejected'),
style: TextButton.styleFrom(foregroundColor: Colors.redAccent),
child: const Text('رفض'),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () => _handleAction(admin['id'], 'approved'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00E5FF),
foregroundColor: Colors.black,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: const Text('موافقة', style: TextStyle(fontWeight: FontWeight.bold)),
),
],
),
],
),
);
},
);
}
}

View File

@@ -0,0 +1,323 @@
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';
import 'package:intl/intl.dart';
class AdvancedAnalyticsPage extends StatelessWidget {
const AdvancedAnalyticsPage({super.key});
@override
Widget build(BuildContext context) {
final controller = Get.put(AnalyticsV2Controller());
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(),
)
],
),
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 _buildSectionTitle(String title) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Text(
title,
style: const TextStyle(
color: AppColor.textPrimary,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
);
}
Widget _buildSummarySection(Map<String, dynamic>? summary) {
if (summary == null) return const SizedBox();
return Row(
children: [
_buildSummaryCard('إجمالي الإيرادات',
'${summary['total_revenue_all'] ?? 0}', AppColor.info),
const SizedBox(width: 12),
_buildSummaryCard('صافي الربح', '${summary['total_profit_all'] ?? 0}',
AppColor.success),
],
);
}
Widget _buildSummaryCard(String title, String value, Color color) {
return Expanded(
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: color.withOpacity(0.3)),
),
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)),
],
),
),
);
}
Widget _buildRevenueChart(List<dynamic> daily) {
if (daily.isEmpty) return const Center(child: Text('لا توجد بيانات'));
List<FlSpot> revenueSpots = [];
List<FlSpot> profitSpots = [];
for (int i = 0; i < daily.length; i++) {
revenueSpots.add(FlSpot(i.toDouble(),
double.tryParse(daily[i]['total_revenue'].toString()) ?? 0));
profitSpots.add(FlSpot(i.toDouble(),
double.tryParse(daily[i]['company_profit'].toString()) ?? 0));
}
return Container(
height: 300,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(24),
),
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();
},
),
),
),
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) {
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('لا توجد بيانات'));
List<BarChartGroupData> barGroups = [];
int maxLength =
passengers.length > drivers.length ? passengers.length : drivers.length;
for (int i = 0; i < maxLength; i++) {
double pCount = i < passengers.length
? double.tryParse(passengers[i]['new_passengers'].toString()) ?? 0
: 0;
double dCount = i < drivers.length
? double.tryParse(drivers[i]['new_drivers'].toString()) ?? 0
: 0;
barGroups.add(
BarChartGroupData(
x: i,
barRods: [
BarChartRodData(
toY: pCount,
color: AppColor.info,
width: 8,
borderRadius: BorderRadius.circular(4)),
BarChartRodData(
toY: dCount,
color: AppColor.warning,
width: 8,
borderRadius: BorderRadius.circular(4)),
],
),
);
}
return Container(
height: 250,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(24),
),
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();
},
),
),
),
),
),
);
}
Widget _buildTopDriversList(List<dynamic> drivers) {
if (drivers.isEmpty) return const Center(child: Text('لا توجد بيانات'));
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: drivers.length,
itemBuilder: (ctx, i) {
final d = drivers[i];
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: [
CircleAvatar(
backgroundColor: AppColor.accent.withOpacity(0.1),
child: Text('${i + 1}',
style: const TextStyle(
color: AppColor.accent, fontWeight: FontWeight.bold)),
),
const SizedBox(width: 16),
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)),
],
),
),
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)),
],
),
],
),
);
},
);
}
}

View File

@@ -0,0 +1,163 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart' hide TextDirection;
import 'package:siro_admin/controller/functions/launch.dart';
import '../../../controller/admin/static_controller.dart';
import '../../widgets/mycircular.dart';
class DailyNotesView extends StatelessWidget {
const DailyNotesView({super.key});
@override
Widget build(BuildContext context) {
// نستخدم نفس الكونترولر للوصول لدالة جلب الملاحظات
final controller = Get.find<StaticController>();
// عند فتح الصفحة، نجلب ملاحظات اليوم الحالي
WidgetsBinding.instance.addPostFrameCallback((_) {
controller.fetchDailyNotes(DateTime.now());
});
return Scaffold(
backgroundColor: const Color(0xFFF0F2F5),
appBar: AppBar(
title: Text(
'سجل المكالمات اليومي',
style: const TextStyle(
color: Color(0xFF1A1A1A),
fontWeight: FontWeight.w800,
fontSize: 20,
),
),
centerTitle: true,
backgroundColor: Colors.white,
elevation: 0,
iconTheme: const IconThemeData(color: Colors.black87),
),
body: GetBuilder<StaticController>(
builder: (controller) {
if (controller.isLoadingNotes) {
return const Center(child: MyCircularProgressIndicator());
}
if (controller.dailyNotesList.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.note_alt_outlined,
size: 80, color: Colors.grey.shade300),
const SizedBox(height: 10),
Text("لا توجد سجلات لهذا اليوم",
style: TextStyle(color: Colors.grey.shade600)),
],
),
);
}
return ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: controller.dailyNotesList.length,
separatorBuilder: (context, index) => const SizedBox(height: 12),
itemBuilder: (context, index) {
final note = controller.dailyNotesList[index];
final String name = note['editor'] ?? note['name'] ?? 'Unknown';
final String phone = note['phone'] ?? note['phone'] ?? 'Unknown';
final String content = note['note'] ?? note['content'] ?? '';
final String time = note['createdAt'] ?? '';
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
blurRadius: 10,
offset: const Offset(0, 4),
)
],
border: Border(
right: BorderSide(
color: _getEmployeeColor(name), width: 4))),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
CircleAvatar(
radius: 14,
backgroundColor:
_getEmployeeColor(name).withOpacity(0.1),
child: Icon(Icons.person,
size: 16, color: _getEmployeeColor(name)),
),
const SizedBox(width: 8),
Text(
name.toUpperCase(),
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.grey.shade800,
fontSize: 14),
),
const SizedBox(width: 100),
InkWell(
onTap: () {
makePhoneCall('+$phone');
},
child: Row(
children: [
Text(
phone,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.grey.shade800,
fontSize: 14),
),
Icon(Icons.phone)
],
),
),
const SizedBox(width: 22),
Text(
time.split(' ').last, // عرض الوقت فقط
style: TextStyle(
color: Colors.grey.shade400, fontSize: 12),
textDirection: TextDirection.ltr,
),
],
),
],
),
const Divider(height: 20),
Text(
content,
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade700,
height: 1.5),
),
],
),
);
},
);
},
),
);
}
Color _getEmployeeColor(String name) {
String n = name.toLowerCase().trim();
if (n.contains('shahd')) return Colors.redAccent;
if (n.contains('mayar')) return Colors.amber.shade700;
if (n.contains('rama2')) return Colors.green;
if (n.contains('rama1')) return Colors.blue;
return Colors.blueGrey;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,83 @@
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';
class Wallet extends StatelessWidget {
Wallet({super.key});
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];
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
}
},
),
)
],
),
);
})
],
isleading: true,
action: IconButton(
onPressed: () async {
walletAdminController.getWalletForEachDriverToPay();
},
icon: const Icon(
Icons.refresh,
color: Colors.black,
),
),
);
}
}