first commit
This commit is contained in:
1123
siro_admin/lib/views/admin/admin_home_page.dart
Normal file
1123
siro_admin/lib/views/admin/admin_home_page.dart
Normal file
File diff suppressed because it is too large
Load Diff
335
siro_admin/lib/views/admin/captain/captain.dart
Normal file
335
siro_admin/lib/views/admin/captain/captain.dart
Normal 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"),
|
||||
);
|
||||
}
|
||||
}
|
||||
425
siro_admin/lib/views/admin/captain/captain_details.dart
Normal file
425
siro_admin/lib/views/admin/captain/captain_details.dart
Normal 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))),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
54
siro_admin/lib/views/admin/captain/drivers_cant_registe.dart
Normal file
54
siro_admin/lib/views/admin/captain/drivers_cant_registe.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
122
siro_admin/lib/views/admin/captain/form_captain.dart
Normal file
122
siro_admin/lib/views/admin/captain/form_captain.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
982
siro_admin/lib/views/admin/captain/register_captain.dart
Normal file
982
siro_admin/lib/views/admin/captain/register_captain.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
287
siro_admin/lib/views/admin/complaints/complaint_list_page.dart
Normal file
287
siro_admin/lib/views/admin/complaints/complaint_list_page.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
339
siro_admin/lib/views/admin/dashboard_v2_widget.dart
Normal file
339
siro_admin/lib/views/admin/dashboard_v2_widget.dart
Normal 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),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
94
siro_admin/lib/views/admin/dashboard_widget.dart
Normal file
94
siro_admin/lib/views/admin/dashboard_widget.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
69
siro_admin/lib/views/admin/drivers/alexandria.dart
Normal file
69
siro_admin/lib/views/admin/drivers/alexandria.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
230
siro_admin/lib/views/admin/drivers/driver_gift_check_page.dart
Normal file
230
siro_admin/lib/views/admin/drivers/driver_gift_check_page.dart
Normal 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),
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
654
siro_admin/lib/views/admin/drivers/driver_the_best.dart
Normal file
654
siro_admin/lib/views/admin/drivers/driver_the_best.dart
Normal 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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
837
siro_admin/lib/views/admin/drivers/driver_tracker_screen.dart
Normal file
837
siro_admin/lib/views/admin/drivers/driver_tracker_screen.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
69
siro_admin/lib/views/admin/drivers/giza.dart
Normal file
69
siro_admin/lib/views/admin/drivers/giza.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
833
siro_admin/lib/views/admin/drivers/monitor_ride.dart
Normal file
833
siro_admin/lib/views/admin/drivers/monitor_ride.dart
Normal 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, // للحفاظ على اتجاه الأرقام
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
564
siro_admin/lib/views/admin/employee/employee_page.dart
Normal file
564
siro_admin/lib/views/admin/employee/employee_page.dart
Normal 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)));
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
);
|
||||
}
|
||||
855
siro_admin/lib/views/admin/enceypt/encrypt.dart
Normal file
855
siro_admin/lib/views/admin/enceypt/encrypt.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
366
siro_admin/lib/views/admin/enceypt/fingerprint_migration.dart
Normal file
366
siro_admin/lib/views/admin/enceypt/fingerprint_migration.dart
Normal 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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
659
siro_admin/lib/views/admin/error/error/error_page.dart
Normal file
659
siro_admin/lib/views/admin/error/error/error_page.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
278
siro_admin/lib/views/admin/financial/financial_v2_page.dart
Normal file
278
siro_admin/lib/views/admin/financial/financial_v2_page.dart
Normal 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)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
603
siro_admin/lib/views/admin/packages.dart
Normal file
603
siro_admin/lib/views/admin/packages.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
86
siro_admin/lib/views/admin/passenger/form_passenger.dart
Normal file
86
siro_admin/lib/views/admin/passenger/form_passenger.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
));
|
||||
}
|
||||
390
siro_admin/lib/views/admin/passenger/passenger.dart
Normal file
390
siro_admin/lib/views/admin/passenger/passenger.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
458
siro_admin/lib/views/admin/passenger/passenger_details_page.dart
Normal file
458
siro_admin/lib/views/admin/passenger/passenger_details_page.dart
Normal 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)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
254
siro_admin/lib/views/admin/pricing/kazan_editor_page.dart
Normal file
254
siro_admin/lib/views/admin/pricing/kazan_editor_page.dart
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
269
siro_admin/lib/views/admin/promo/promo_management_page.dart
Normal file
269
siro_admin/lib/views/admin/promo/promo_management_page.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
153
siro_admin/lib/views/admin/quality/blacklist_page.dart
Normal file
153
siro_admin/lib/views/admin/quality/blacklist_page.dart
Normal 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();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
240
siro_admin/lib/views/admin/quality/driver_scorecard_page.dart
Normal file
240
siro_admin/lib/views/admin/quality/driver_scorecard_page.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
1213
siro_admin/lib/views/admin/rides/ride_lookup_page.dart
Normal file
1213
siro_admin/lib/views/admin/rides/ride_lookup_page.dart
Normal file
File diff suppressed because it is too large
Load Diff
236
siro_admin/lib/views/admin/rides/rides.dart
Normal file
236
siro_admin/lib/views/admin/rides/rides.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
))
|
||||
]);
|
||||
}
|
||||
}
|
||||
121
siro_admin/lib/views/admin/security/audit_logs_page.dart
Normal file
121
siro_admin/lib/views/admin/security/audit_logs_page.dart
Normal 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)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
669
siro_admin/lib/views/admin/server/monitor_server_page.dart
Normal file
669
siro_admin/lib/views/admin/server/monitor_server_page.dart
Normal 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"),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
195
siro_admin/lib/views/admin/staff/add_staff_page.dart
Normal file
195
siro_admin/lib/views/admin/staff/add_staff_page.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
148
siro_admin/lib/views/admin/staff/pending_admins_page.dart
Normal file
148
siro_admin/lib/views/admin/staff/pending_admins_page.dart
Normal 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)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
323
siro_admin/lib/views/admin/static/advanced_analytics_page.dart
Normal file
323
siro_admin/lib/views/admin/static/advanced_analytics_page.dart
Normal 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)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
163
siro_admin/lib/views/admin/static/notes_driver_page.dart
Normal file
163
siro_admin/lib/views/admin/static/notes_driver_page.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
1326
siro_admin/lib/views/admin/static/static.dart
Normal file
1326
siro_admin/lib/views/admin/static/static.dart
Normal file
File diff suppressed because it is too large
Load Diff
83
siro_admin/lib/views/admin/wallet/wallet.dart
Normal file
83
siro_admin/lib/views/admin/wallet/wallet.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user