This commit is contained in:
Hamza-Ayed
2026-05-01 01:43:59 +03:00
parent cdda136006
commit 5fc160e374
25 changed files with 1526 additions and 1856 deletions

View File

@@ -1,14 +1,46 @@
import 'package:flutter/material.dart';
import 'package:flutter/material.dart';
class AppColor {
static const Color primaryColor = Colors.black;
static const Color secondaryColor = Colors.white;
static const Color accentColor = Colors.grey;
static const Color redColor = Color(0xFFEA4335); // Google Red
static const Color greenColor = Color(0xFF34A853); // Google Green
static const Color blueColor =
Color.fromARGB(255, 66, 135, 246); // Google Blue
static const Color yellowColor = Color(0xFFFBBC05); // Google Yellow
static Color deepPurpleAccent =
const Color.fromARGB(255, 123, 76, 254).withOpacity(0.3);
// --- Core Design Tokens ---
// Background & Surfaces
static const Color bg = Color(0xFF0A0A0B);
static const Color surface = Color(0xFF161618);
static const Color surfaceElevated = Color(0xFF222225);
static const Color surfaceGlass = Color(0xCC161618);
// Accents & Branding
static const Color accent = Color(0xFF6366F1); // Indigo / Violet
static const Color accentSoft = Color(0x266366F1); // 15% Opacity
static const Color accentBorder = Color(0x4D6366F1); // 30% Opacity
static const Color glow = Color(0xFF818CF8);
// Semantic / State Colors
static const Color danger = Color(0xFFEF4444);
static const Color dangerSoft = Color(0x26EF4444);
static const Color success = Color(0xFF10B981);
static const Color successSoft = Color(0x2610B981);
static const Color warning = Color(0xFFF59E0B);
static const Color info = Color(0xFF3B82F6);
// Text & Content
static const Color textPrimary = Color(0xFFF3F4F6);
static const Color textSecondary = Color(0xFF9CA3AF);
static const Color textMuted = Color(0xFF6B7280);
// UI Elements
static const Color divider = Color(0xFF2D2D30);
static const Color cardShadow = Color(0x66000000);
// --- Legacy Mappings (for temporary compatibility) ---
static const Color primaryColor = bg;
static const Color secondaryColor = textPrimary;
static const Color accentColor = accent;
static const Color redColor = danger;
static const Color greenColor = success;
static const Color blueColor = info;
static const Color yellowColor = warning;
}

View File

@@ -11,7 +11,7 @@ class AC {
gAK() async {
if (box.read(BoxName.apiKeyRun).toString() != 'run') {
var res = await CRUD().get(link: AppLink.getApiKey, payload: {});
var decod = jsonDecode(res);
var decod = res is String ? jsonDecode(res) : res;
print(decod);
Map<String, dynamic> jsonData = {};
for (var i = 0; i < decod['message'].length; i++) {

View File

@@ -11,7 +11,15 @@ class AppLink {
// static final String endPoint = box.read(BoxName.serverChosen);
// static final String server = Env.seferCairoServer;
static final String server = 'https://api.intaleq.xyz/intaleq_v1';
static final String server = 'https://api.intaleq.xyz/intaleq_v3';
static final String endPoint = 'https://api.intaleq.xyz/intaleq_v3';
static final String syria = 'https://syria.intaleq.xyz/intaleq';
static String paymentServer = 'https://walletintaleq.intaleq.xyz/v1/main';
static String locationServer = 'https://location.intaleq.xyz/intaleq/ride/location';
static String locationServerSide = 'https://location.intaleq.xyz/intaleq/ride/location';
static String mapSaasRoute = 'https://map-saas.intaleqapp.com/api/maps/route';
static String mapSaasPlaces = 'https://map-saas.intaleqapp.com/api/geocoding/places';
static const String routeApiBaseUrl = "https://routesjo.intaleq.xyz/route/v1/driving";
static String loginJwtDriver =
"https://api.intaleq.xyz/intaleq/loginAdmin.php";
//=============================
@@ -35,6 +43,7 @@ class AppLink {
static String test = "$server/test.php";
static String loginWalletAdmin = "$seferPaymentServer/loginWalletAdmin.php";
static String loginWalletAdminV3 = "$server/Admin/auth/loginWallet.php";
//===============firebase==========================
static String getTokens = "$server/ride/firebase/get.php";
static String getInvoices = "$server/Admin/adminUser/invoice_total.php";
@@ -209,7 +218,7 @@ class AppLink {
static String uploadEgypt = "$server/uploadEgypt.php";
//==================certifcate==========
static String location = '$server/ride/location';
static String location = locationServer;
static String getCarsLocationByPassenger = "$location/get.php";
static String getFemalDriverLocationByPassenger =
"$location/getFemalDriver.php";
@@ -277,6 +286,7 @@ class AppLink {
"$server/auth/syria/driver/driver_details.php";
static String deleteCaptain = "$server/Admin/driver/deleteCaptain.php";
static String addAdminUser = "$server/Admin/adminUser/add.php";
static String addStaff = "$server/Admin/Staff/add.php";
static String getdashbord = "$server/Admin/dashbord.php";
static String getEmployee = "$server/Admin/employee/get.php";
static String getBestDriver = "$server/Admin/driver/getBestDriver.php";

View File

@@ -6,59 +6,81 @@ import 'box_name.dart';
import 'colors.dart';
class AppStyle {
static TextStyle headTitle = const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 40,
color: AppColor.accentColor,
);
static TextStyle headTitle2 = const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 26,
color: AppColor.primaryColor,
);
static TextStyle title = const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: AppColor.primaryColor,
);
static TextStyle subtitle = const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 13,
color: AppColor.primaryColor,
);
static TextStyle number = const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: AppColor.primaryColor,
// --- Typography ---
static TextStyle display = GoogleFonts.inter(
fontWeight: FontWeight.w800,
fontSize: 32,
color: AppColor.textPrimary,
letterSpacing: -1,
);
static BoxDecoration boxDecoration = const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(12)),
color: AppColor.secondaryColor,
boxShadow: [
BoxShadow(
color: AppColor.accentColor,
offset: Offset(-3, -3),
blurRadius: 0,
spreadRadius: 0,
blurStyle: BlurStyle.outer),
BoxShadow(
color: AppColor.accentColor,
offset: Offset(3, 3),
blurRadius: 0,
spreadRadius: 0,
blurStyle: BlurStyle.outer)
]);
static BoxDecoration boxDecoration1 = const BoxDecoration(
boxShadow: [
BoxShadow(
color: AppColor.accentColor, blurRadius: 5, offset: Offset(2, 4)),
BoxShadow(
color: AppColor.accentColor, blurRadius: 5, offset: Offset(-2, -2))
],
color: AppColor.secondaryColor,
borderRadius: BorderRadius.all(
Radius.elliptical(15, 30),
),
static TextStyle headTitle = GoogleFonts.cairo(
fontWeight: FontWeight.bold,
fontSize: 24,
color: AppColor.textPrimary,
);
static TextStyle title = GoogleFonts.inter(
fontWeight: FontWeight.w600,
fontSize: 16,
color: AppColor.textPrimary,
);
static TextStyle subtitle = GoogleFonts.inter(
fontWeight: FontWeight.w500,
fontSize: 14,
color: AppColor.textSecondary,
);
static TextStyle body = GoogleFonts.inter(
fontWeight: FontWeight.normal,
fontSize: 14,
color: AppColor.textPrimary,
);
static TextStyle caption = GoogleFonts.inter(
fontWeight: FontWeight.w400,
fontSize: 12,
color: AppColor.textMuted,
);
static TextStyle number = GoogleFonts.jetBrainsMono(
fontWeight: FontWeight.bold,
fontSize: 15,
color: AppColor.accent,
);
// --- Decorations ---
static BoxDecoration cardDecoration = BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColor.divider, width: 1),
boxShadow: const [
BoxShadow(
color: AppColor.cardShadow,
blurRadius: 20,
offset: Offset(0, 8),
),
],
);
static BoxDecoration elevatedCard = BoxDecoration(
color: AppColor.surfaceElevated,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColor.accentBorder, width: 1),
);
static BoxDecoration glassDecoration = BoxDecoration(
color: AppColor.surfaceGlass,
borderRadius: BorderRadius.circular(24),
border: Border.all(color: AppColor.divider, width: 1),
);
// --- Legacy Mappings ---
static TextStyle headTitle2 = headTitle;
static BoxDecoration boxDecoration = cardDecoration;
static BoxDecoration boxDecoration1 = elevatedCard;
}

View File

@@ -25,10 +25,16 @@ class DashboardController extends GetxController {
var res = await CRUD().get(link: AppLink.getdashbord, payload: {});
print('📡 Main dashboard response: $res');
if (res != 'failure') {
var d = jsonDecode(res);
print('✅ Decoded main dashboard: ${jsonEncode(d)}');
dashbord = d['message'];
if (res != 'failure' && res != null) {
try {
var d = res is String ? jsonDecode(res) : res;
print('✅ Decoded main dashboard: ${jsonEncode(d)}');
if (d['status'] == 'success' && d['message'] != null) {
dashbord = d['message'] is List ? d['message'] : [d['message']];
}
} catch (e) {
print('❌ Error parsing main dashboard: $e');
}
} else {
print('❌ Failed to load main dashboard');
}
@@ -40,7 +46,7 @@ class DashboardController extends GetxController {
);
print('💳 Wallet dashboard response: $resPayments');
if (resPayments != 'failure') {
if (resPayments is Map && resPayments['status'] == 'success') {
var p = resPayments;
print('✅ Decoded wallet dashboard: ${jsonEncode(p)}');
@@ -48,9 +54,11 @@ class DashboardController extends GetxController {
p['message'] is List &&
p['message'].isNotEmpty) {
dashbord[0].addAll(p['message'][0]);
} else if (dashbord.isNotEmpty && p['message'] is Map) {
dashbord[0].addAll(p['message']);
}
} else {
print('❌ Failed to load wallet dashboard');
print('❌ Failed to load wallet dashboard (or verification required)');
}
// 🔹 Check SMS credit

View File

@@ -0,0 +1,81 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../constant/links.dart';
import '../functions/crud.dart';
import '../functions/device_info.dart';
import '../../views/widgets/snackbar.dart';
class StaffController extends GetxController {
final CRUD _crud = CRUD();
final formKey = GlobalKey<FormState>();
// التكست كنترولرز
final nameController = TextEditingController();
final phoneController = TextEditingController();
final emailController = TextEditingController();
final passwordController = TextEditingController();
final birthdateController = TextEditingController();
String selectedGender = 'Male';
String selectedRole = 'service'; // 'admin' or 'service'
bool isLoading = false;
Future<void> registerStaff() async {
if (!formKey.currentState!.validate()) return;
isLoading = true;
update();
try {
// ملاحظة: لا نأخذ بصمة جهاز الأدمن هنا، بل نتركها فارغة ليقوم الموظف بربطها عند أول دخول له
String fingerprint = "";
var response = await _crud.post(
link: AppLink.addStaff,
payload: {
"name": nameController.text.trim(),
"phone": phoneController.text.trim(),
"email": emailController.text.trim(),
"password": passwordController.text.trim(),
"role": selectedRole,
"gender": selectedGender,
"birthdate": birthdateController.text.trim(),
"fingerprint": fingerprint,
"site": "main", // القيمة الافتراضية للفرع
},
);
if (response != "failure") {
mySnackbarSuccess('تمت إضافة الموظف بنجاح');
_clearFields();
Get.back();
} else {
mySnackeBarError('فشل في إضافة الموظف. يرجى المحاولة لاحقاً');
}
} catch (e) {
mySnackeBarError('حدث خطأ: $e');
} finally {
isLoading = false;
update();
}
}
void _clearFields() {
nameController.clear();
phoneController.clear();
emailController.clear();
passwordController.clear();
birthdateController.clear();
}
@override
void onClose() {
nameController.dispose();
phoneController.dispose();
emailController.dispose();
passwordController.dispose();
birthdateController.dispose();
super.onClose();
}
}

View File

@@ -1,14 +1,18 @@
import 'package:flutter/material.dart';
import 'package:jwt_decoder/jwt_decoder.dart';
import 'package:get/get.dart';
import 'package:sefer_admin1/controller/functions/device_info.dart';
import 'package:sefer_admin1/views/auth/login_page.dart';
import '../../constant/box_name.dart';
import '../../constant/links.dart';
import '../../constant/info.dart';
import '../../main.dart';
import '../../print.dart';
import '../../views/admin/admin_home_page.dart';
import '../../views/widgets/snackbar.dart';
import '../functions/crud.dart';
import '../functions/encrypt_decrypt.dart';
class OtpHelper extends GetxController {
static final String _sendOtpUrl =
@@ -73,27 +77,181 @@ class OtpHelper extends GetxController {
}
}
Future<void> checkAdminLogin() async {
final deviceNumber =
box.read(BoxName.fingerPrint); // خزّنه عند التشغيل أول مرة
final phoneNumber = box.read(BoxName.adminPhone); // عند التحقق من OTP
if (deviceNumber != null && phoneNumber != null) {
/// تسجيل الدخول بكلمة المرور والبصمة
Future<bool> loginWithPassword(String password) async {
try {
final fingerprint = box.read(BoxName.fingerPrint);
final response = await CRUD().post(
link: _checkAdminLogin,
payload: {
"device_number": deviceNumber,
"phone_number": phoneNumber,
'fingerprint': fingerprint,
'password': password,
},
);
if (response != "failure") {
Get.offAll(() => AdminHomePage());
if (response != 'failure') {
// إذا كان الرد يتطلب OTP (السيرفر يرجعها بداخل message)
final msg = response['message'];
if (response['status'] == 'otp_required' || (msg is Map && msg['status'] == 'otp_required')) {
String phone = (msg is Map ? msg['phone'] : response['phone']) ?? '';
_showOtpDialog(phone, password, fingerprint);
return false; // ننتظر إكمال الـ OTP
}
// إذا نجح الدخول مباشرة
return _handleLoginSuccess(response, password);
} else {
Get.offAll(() => AdminLoginPage());
// سيقوم CRUD بإظهار الخطأ المناسب (مثل "حسابك قيد المراجعة")
return false;
}
} catch (e) {
Log.print('LOGIN ERROR: $e');
mySnackeBarError('حدث خطأ أثناء تسجيل الدخول: $e');
return false;
}
}
/// التحقق من OTP الخاص بتسجيل الدخول
Future<void> verifyLoginOtp(
String phone, String otp, String password, String fingerprint) async {
try {
final response = await CRUD().post(
link: '${AppLink.server}/Admin/auth/verify_login.php',
payload: {
'phone': phone,
'otp': otp,
'fingerprint': fingerprint,
},
);
if (response != 'failure') {
bool success = await _handleLoginSuccess(response, password);
if (success) {
Get.offAll(() => const AdminHomePage());
}
}
} catch (e) {
Log.print('OTP VERIFY LOGIN ERROR: $e');
mySnackeBarError('خطأ في التحقق من الرمز: $e');
}
}
Future<bool> _handleLoginSuccess(dynamic response, String password) async {
final msg = response['message'];
final data = response['admin'] ?? (msg is Map ? msg['admin'] : null);
final jwt = response['jwt'] ?? (msg is Map ? msg['jwt'] : null);
if (jwt != null) {
await box.write(BoxName.jwt, c(jwt));
}
if (data != null) {
if (data['id'] != null) await box.write(BoxName.driverID, data['id']);
if (data['role'] != null) {
String role = data['role'].toString().trim();
await box.write('admin_role', role);
Log.print('Admin role saved: $role');
}
if (data['phone'] != null) await box.write(BoxName.adminPhone, data['phone']);
}
await box.write(BoxName.phoneVerified, true);
await box.write('admin_password', password);
mySnackbarSuccess('تم تسجيل الدخول بنجاح');
return true;
}
void _showOtpDialog(String phone, String password, String fingerprint) {
String otpCode = '';
Get.defaultDialog(
title: 'رمز التحقق',
content: Column(
children: [
Text('تم إرسال رمز التحقق إلى WhatsApp الخاص بك ($phone)'),
const SizedBox(height: 16),
TextField(
onChanged: (val) => otpCode = val,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
hintText: 'أدخل الرمز هنا',
border: OutlineInputBorder(),
),
),
],
),
textConfirm: 'تحقق',
confirmTextColor: Colors.white,
onConfirm: () {
if (otpCode.length >= 5) {
Get.back();
verifyLoginOtp(phone, otpCode, password, fingerprint);
} else {
mySnackeBarError('الرجاء إدخال رمز صحيح');
}
},
);
}
static bool _isAutoLoginAttempted = false;
Future<void> checkAdminLogin() async {
final fingerprint = box.read(BoxName.fingerPrint);
final password = box.read('admin_password');
if (fingerprint == null || password == null) {
Get.offAll(() => const AdminLoginPage());
return;
}
// ─── أولاً: التحقق من وجود توكن JWT صالح ─────────
// إذا وُجد توكن غير منتهي الصلاحية → ندخل مباشرة بدون استدعاء login.php
final rawJwt = box.read(BoxName.jwt);
if (rawJwt != null) {
try {
String token = r(rawJwt.toString()).split(AppInformation.addd)[0];
if (!JwtDecoder.isExpired(token)) {
Log.print('Valid JWT found, skipping login.php (no OTP needed)');
Get.offAll(() => const AdminHomePage());
return;
}
Log.print('JWT expired, need fresh login');
} catch (e) {
Log.print('JWT decode failed: \$e, need fresh login');
}
}
// ─── ثانياً: لا يوجد توكن صالح → استدعاء login.php ───
final response = await CRUD().post(
link: _checkAdminLogin,
payload: {
'fingerprint': fingerprint,
'password': password,
'is_renewal': '1',
},
);
if (response != 'failure') {
final msg = response['message'];
if (response['status'] == 'otp_required' || (msg is Map && msg['status'] == 'otp_required')) {
String phone = (msg is Map ? msg['phone'] : response['phone']) ?? '';
_showOtpDialog(phone, password, fingerprint);
return; // ننتظر إدخال رمز التحقق
}
if (msg is Map && msg['jwt'] != null) {
box.write(BoxName.jwt, c(msg['jwt']));
if (msg['admin'] != null && msg['admin']['id'] != null) {
box.write(BoxName.driverID, msg['admin']['id']);
}
} else if (response['jwt'] != null) {
box.write(BoxName.jwt, c(response['jwt']));
}
Get.offAll(() => const AdminHomePage());
} else {
Get.offAll(() => AdminLoginPage());
Log.print('Auto-login failed, redirecting to login page');
Get.offAll(() => const AdminLoginPage());
}
}
@@ -104,7 +262,15 @@ class OtpHelper extends GetxController {
box.write(BoxName.fingerPrint, deviceFingerprint);
});
// تأجيل تنفيذ التنقل حتى تنتهي مرحلة البناء
// ⚠️ محاولة واحدة فقط للتسجيل التلقائي لمنع الحلقة اللانهائية
Future.microtask(() {
if (_isAutoLoginAttempted) {
// نحن في حلقة — نتوقف ونذهب لصفحة الدخول مباشرة
Log.print('Auto-login already attempted, skipping to login page');
return;
}
_isAutoLoginAttempted = true;
if (box.read(BoxName.phoneVerified) == true &&
box.read(BoxName.adminPhone) != null) {
checkAdminLogin();

View File

@@ -18,11 +18,17 @@ class EmployeeController extends GetxController {
fetchEmployee() async {
var res = await CRUD().get(link: AppLink.getEmployee, payload: {});
if (res != 'failure') {
employee = jsonDecode(res)['message'];
if (res is String && (res == 'failure' || res == 'token_expired')) {
Get.snackbar('error', 'Failed to load employees', backgroundColor: AppColor.redColor);
return;
}
try {
var jsonData = res is String ? jsonDecode(res) : res;
employee = jsonData['message'];
update();
} else {
Get.snackbar('error', '', backgroundColor: AppColor.redColor);
} catch (e) {
Get.snackbar('error', 'Invalid server response', backgroundColor: AppColor.redColor);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -29,22 +29,19 @@ class DeviceHelper {
throw UnsupportedError('Unsupported platform');
}
// Extract relevant device information
final String deviceId = Platform.isAndroid
? deviceData['fingerprint'] ?? 'unknown'
: deviceData['identifierForVendor'] ?? 'unknown';
String deviceId = 'unknown';
if (Platform.isAndroid) {
deviceId = deviceData['fingerprint'] ?? 'unknown';
} else if (Platform.isIOS) {
deviceId = deviceData['identifierForVendor'] ?? 'unknown';
} else if (Platform.isMacOS) {
deviceId = deviceData['systemGUID'] ?? 'unknown';
}
final String deviceModel = deviceData['model'] ?? 'unknown';
final String osVersion = Platform.isAndroid
? deviceData['version']['release'] ?? 'unknown'
: deviceData['systemVersion'] ?? 'unknown';
// Log the extracted information
// Generate and return the encrypted fingerprint
final String fingerprint = '${deviceId}_${deviceModel}_$osVersion';
// print(EncryptionHelper.instance.encryptData(fingerprint));
final String fingerprint = '${deviceId}_$deviceModel';
print(fingerprint);
return (fingerprint);
} catch (e) {
throw Exception('Failed to generate device fingerprint');

22
lib/debug_jwt.dart Normal file
View File

@@ -0,0 +1,22 @@
import 'package:sefer_admin1/controller/functions/encrypt_decrypt.dart';
import 'package:sefer_admin1/constant/info.dart';
void main() {
String testJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiYWRtaW4ifQ.signature";
print("Original: $testJwt");
String encrypted = c(testJwt);
print("Encrypted: $encrypted");
String decrypted = r(encrypted);
print("Decrypted: $decrypted");
String split = decrypted.split(AppInformation.addd)[0];
print("Split: $split");
if (testJwt == split) {
print("✅ Match!");
} else {
print("❌ Mismatch!");
}
}

View File

@@ -8,6 +8,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:sefer_admin1/views/auth/login_page.dart';
import 'package:sefer_admin1/views/auth/register_page.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'controller/firebase/firbase_messge.dart';
import 'controller/functions/encrypt_decrypt.dart';
@@ -62,7 +63,12 @@ class MainApp extends StatelessWidget {
Widget build(BuildContext context) {
return GetMaterialApp(
debugShowCheckedModeBanner: false,
home: AdminLoginPage(),
initialRoute: '/login',
getPages: [
GetPage(name: '/login', page: () => const AdminLoginPage()),
GetPage(name: '/register', page: () => const RegisterPage()),
],
home: const AdminLoginPage(),
);
}
}

View File

@@ -13,6 +13,7 @@ import '../../controller/admin/static_controller.dart';
import '../../controller/functions/crud.dart';
import '../../controller/notification_controller.dart';
import '../../main.dart';
import '../../print.dart';
import '../invoice/invoice_list_page.dart';
import 'captain/captain.dart';
import 'captain/syrian_driver_not_active.dart';
@@ -28,6 +29,8 @@ import 'rides/ride_lookup_page.dart';
import 'server/monitor_server_page.dart';
import 'static/static.dart';
import 'wallet/wallet.dart';
import 'staff/add_staff_page.dart';
import 'staff/pending_admins_page.dart';
class AdminHomePage extends StatefulWidget {
const AdminHomePage({super.key});
@@ -69,10 +72,16 @@ class _AdminHomePageState extends State<AdminHomePage>
duration: const Duration(seconds: 2),
)..repeat(reverse: true);
String myPhone = box.read(BoxName.adminPhone).toString();
isSuperAdmin = myPhone == '201023248456' ||
myPhone == '963992952235' ||
myPhone == '963942542053';
final String role = box.read('admin_role')?.toString() ?? 'admin';
final String myPhone = box.read(BoxName.adminPhone)?.toString() ?? '';
// التحقق من الصلاحيات: إما عن طريق الدور أو عن طريق قائمة أرقام السوبر أدمن التقليدية
isSuperAdmin = (role == 'super_admin') ||
(myPhone == '201023248456' ||
myPhone == '963992952235' ||
myPhone == '963942542053');
Log.print('AdminHomePage: role=$role, isSuperAdmin=$isSuperAdmin');
dashboardController = Get.put(DashboardController());
}
@@ -760,6 +769,8 @@ class _AdminHomePageState extends State<AdminHomePage>
const Color(0xFF80CBC4), () => Get.to(() => InvoiceListPage())),
ActionItem('الموظفون', Icons.badge_rounded, const Color(0xFFB0BEC5),
() => Get.to(() => EmployeePage())),
ActionItem('موافقة المشرفين', Icons.how_to_reg_rounded, _accent,
() => Get.to(() => const PendingAdminsPage())),
],
),
if (isSuperAdmin)
@@ -804,19 +815,37 @@ class _AdminHomePageState extends State<AdminHomePage>
))),
],
),
if (isSuperAdmin)
ActionCategory(
title: 'إدارة الكوادر',
items: [
ActionItem(
'إضافة مدير',
Icons.admin_panel_settings_rounded,
_accent,
() => Get.to(() => const AddStaffPage(role: 'admin')),
),
ActionItem(
'إضافة خدمة عملاء',
Icons.support_agent_rounded,
_info,
() => Get.to(() => const AddStaffPage(role: 'service')),
),
],
),
];
}
List<Map<String, dynamic>> _getDetailedStats(
dynamic data, DashboardController controller) {
return [
// if (isSuperAdmin)
// {
// 'title': 'رصيد الرسائل',
// 'value': controller.creditSMS,
// 'icon': Icons.sms_rounded,
// 'color': _info,
// },
if (isSuperAdmin)
{
'title': 'رصيد الرسائل',
'value': controller.creditSMS,
'icon': Icons.sms_rounded,
'color': _info,
},
{
'title': 'مكتملة',
'value': data['completed_rides'],

View File

@@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:sefer_admin1/controller/functions/crud.dart';
import 'package:sefer_admin1/controller/functions/wallet.dart'; // تأكد من المسار
import 'package:sefer_admin1/controller/functions/wallet.dart';
import '../../../constant/links.dart'; // تأكد من المسار
// --- Controller: المسؤول عن المنطق (البحث، الفحص، الإضافة) ---
class DriverGiftCheckerController extends GetxController {
@@ -25,8 +27,7 @@ class DriverGiftCheckerController extends GetxController {
Future<void> fetchDriverCache() async {
try {
final response = await CRUD().post(
link:
'https://api.intaleq.xyz/intaleq/Admin/driver/getDriverGiftPayment.php',
link: '${AppLink.server}/Admin/driver/getDriverGiftPayment.php',
payload: {'phone': phoneController.text.trim()},
);
// print('response: ${response}');
@@ -55,7 +56,17 @@ class DriverGiftCheckerController extends GetxController {
try {
// الخطوة 1: استخراج الـ ID بناءً على رقم الهاتف
var driver = driversCache.firstWhere(
(d) => d['phone'].toString().contains(phoneInput),
(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,
);

View File

@@ -9,6 +9,7 @@ import 'package:sefer_admin1/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 {
@@ -107,12 +108,17 @@ class _IntaleqTrackerScreenState extends State<IntaleqTrackerScreen>
try {
String updateUrl =
"${_baseDir}getUpdatedLocationForAdmin.php?mode=${isLiveMode ? 'live' : 'day'}";
await http.get(Uri.parse(updateUrl));
print("📡 Calling Update URL: $updateUrl");
var responseUpdate = await CRUD().post(link: updateUrl, payload: {});
print("📡 Update Response: $responseUpdate");
String v = DateTime.now().millisecondsSinceEpoch.toString();
final responseLive =
await http.get(Uri.parse("${_baseDir}locations_live.json?v=$v"));
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'))
@@ -125,8 +131,11 @@ class _IntaleqTrackerScreenState extends State<IntaleqTrackerScreen>
});
}
final responseDay =
await http.get(Uri.parse("${_baseDir}locations_day.json?v=$v"));
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'))

View File

@@ -20,7 +20,9 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:sefer_admin1/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';
@@ -240,8 +242,7 @@ class _FingerprintMigrationToolState extends State<FingerprintMigrationTool> {
Container(
child: TextButton(
onPressed: () {
print(EncryptionHelper.instance.decryptData(
'dab40749cdecbfddf4696566448b384f0d272705b08b4ff779e085fbf3257026'));
print(EncryptionHelper.instance.decryptData('hbgbitbXrXrBr'));
},
child: Text(
"Decrypt Test",
@@ -259,6 +260,16 @@ class _FingerprintMigrationToolState extends State<FingerprintMigrationTool> {
),
),
),
Container(
child: TextButton(
onPressed: () {
print(r('hbgbitbXrXrBr'));
},
child: Text(
"decrypt X.r",
),
),
),
const SizedBox(height: 24),

View File

@@ -537,11 +537,19 @@ class PackageController extends GetxController {
fetchPackages() async {
isLoading.value = true;
var response = await CRUD().get(link: AppLink.getPackages, payload: {});
if (response != 'failure') {
var jsonData = jsonDecode(response);
packages = jsonData['message'];
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();
Log.print('jsonData: $jsonData');
} catch (e) {
Log.print('❌ Error parsing packages: $e');
}
isLoading.value = false;
}

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ import '../../controller/auth/login_controller.dart';
import '../../controller/auth/otp_helper.dart';
import '../../controller/functions/crud.dart';
import '../../print.dart';
import '../admin/admin_home_page.dart';
// ─── Colors (نفس نظام الألوان المستخدم في التطبيق) ──────────────────────────
class _C {
@@ -31,30 +32,28 @@ class AdminLoginPage extends StatefulWidget {
class _AdminLoginPageState extends State<AdminLoginPage>
with SingleTickerProviderStateMixin {
final _phoneController = TextEditingController();
final _passwordController = TextEditingController();
final _formKey = GlobalKey<FormState>();
bool _isLoading = false;
late final AnimationController _glowCtrl;
late final Animation<double> _glowAnim;
// ─── Logic (بدون تغيير) ────────────────────────────────────────────────────
Future<void> _submit() async {
final allowedPhones = Env.ALLOWED_ADMIN_PHONES;
Log.print('allowedPhones: ${allowedPhones}');
allowedPhones.toString().split(',');
final password = _passwordController.text.trim();
final phone = _phoneController.text.trim();
if (!allowedPhones.contains(phone)) {
Get.snackbar('رفض الدخول', 'رقم الهاتف غير مخوّل بالدخول إلى الإدارة');
if (password.isEmpty) {
Get.snackbar('خطأ', 'يرجى إدخال كلمة المرور');
return;
}
setState(() => _isLoading = true);
final otpSent = await OtpHelper.sendOtp(phone);
if (otpSent) {
Get.to(() => OtpVerificationAdmin(phone: phone));
final otpHelper = Get.find<OtpHelper>();
bool success = await otpHelper.loginWithPassword(password);
if (success) {
Get.offAll(() => const AdminHomePage());
}
setState(() => _isLoading = false);
@@ -75,7 +74,7 @@ class _AdminLoginPageState extends State<AdminLoginPage>
}
void _initializeToken() async {
await CRUD().getJWT();
// await CRUD().getJWT();
}
@override
@@ -168,7 +167,7 @@ class _AdminLoginPageState extends State<AdminLoginPage>
),
const SizedBox(height: 8),
const Text(
'أدخل رقم هاتفك للمتابعة',
'أدخل كلمة المرور للمتابعة',
style: TextStyle(
color: _C.textSec,
fontSize: 14,
@@ -195,14 +194,15 @@ class _AdminLoginPageState extends State<AdminLoginPage>
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// ── Field label ─────────────────────────────
const SizedBox(height: 20),
// ── Field label (Password) ─────────────────────────────
const Row(
children: [
Icon(Icons.phone_android_rounded,
Icon(Icons.lock_outline_rounded,
color: _C.accent, size: 16),
SizedBox(width: 8),
Text(
'رقم الهاتف',
'كلمة المرور',
style: TextStyle(
color: _C.textSec,
fontSize: 13,
@@ -213,28 +213,19 @@ class _AdminLoginPageState extends State<AdminLoginPage>
],
),
const SizedBox(height: 10),
// ── Phone field ─────────────────────────────
// ── Password field ─────────────────────────────
TextFormField(
controller: _phoneController,
keyboardType: TextInputType.phone,
textDirection: TextDirection.ltr,
controller: _passwordController,
obscureText: true,
style: const TextStyle(
color: _C.textPrimary,
fontSize: 16,
fontFamily: 'monospace',
letterSpacing: 1.2,
),
decoration: InputDecoration(
hintText: '+963 XXX XXX XXX',
hintStyle: const TextStyle(
color: _C.textSec,
fontSize: 14,
letterSpacing: 0.5),
hintText: '••••••••',
hintStyle: const TextStyle(color: _C.textSec),
filled: true,
fillColor: _C.inputBg,
prefixIcon: const Icon(Icons.dialpad_rounded,
color: _C.accentDim, size: 20),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide.none,
@@ -249,24 +240,12 @@ class _AdminLoginPageState extends State<AdminLoginPage>
borderSide: const BorderSide(
color: _C.accent, width: 1.5),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(
color: _C.error, width: 1),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(
color: _C.error, width: 1.5),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 16),
errorStyle: const TextStyle(
color: _C.error, fontSize: 12),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'الرجاء إدخال رقم الهاتف';
return 'الرجاء إدخال كلمة المرور';
}
return null;
},
@@ -296,6 +275,21 @@ class _AdminLoginPageState extends State<AdminLoginPage>
const SizedBox(height: 32),
// ── Register Button ─────────────────────────────────
TextButton(
onPressed: () => Get.toNamed('/register'),
child: const Text(
'ليس لديك حساب؟ طلب انضمام',
style: TextStyle(
color: _C.accent,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(height: 16),
// ── Footer ──────────────────────────────────────────
Row(
mainAxisAlignment: MainAxisAlignment.center,
@@ -435,8 +429,8 @@ class _SubmitButtonState extends State<_SubmitButton>
children: [
Icon(Icons.send_rounded, color: Colors.white, size: 18),
SizedBox(width: 10),
Text(
'إرسال رمز التحقق',
const Text(
'تسجيل الدخول',
style: TextStyle(
color: Colors.white,
fontSize: 16,

View File

@@ -0,0 +1,181 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../constant/box_name.dart';
import '../../constant/links.dart';
import '../../main.dart';
import '../../views/widgets/snackbar.dart';
import '../../controller/functions/crud.dart';
class _C {
static const bg = Color(0xFF0A0D14);
static const card = Color(0xFF161D2E);
static const accent = Color(0xFF00E5FF);
static const textPrimary = Color(0xFFE8F0FE);
static const textSec = Color(0xFF7A8BAA);
static const inputBg = Color(0xFF0C1120);
}
class RegisterPage extends StatefulWidget {
const RegisterPage({super.key});
@override
State<RegisterPage> createState() => _RegisterPageState();
}
class _RegisterPageState extends State<RegisterPage> {
final _nameController = TextEditingController();
final _phoneController = TextEditingController();
final _passwordController = TextEditingController();
final _formKey = GlobalKey<FormState>();
bool _isLoading = false;
Future<void> _register() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _isLoading = true);
try {
final fingerprint = box.read(BoxName.fingerPrint);
final response = await CRUD().post(
link: '${AppLink.server}/Admin/auth/register.php',
payload: {
'name': _nameController.text.trim(),
'phone': _phoneController.text.trim(),
'password': _passwordController.text.trim(),
'fingerprint': fingerprint,
},
);
if (response != 'failure') {
mySnackbarSuccess(response['message'] ?? 'تم تقديم طلبك بنجاح');
Get.back(); // العودة لصفحة الدخول
}
} catch (e) {
mySnackeBarError('حدث خطأ أثناء التسجيل: $e');
} finally {
setState(() => _isLoading = false);
}
}
@override
void dispose() {
_nameController.dispose();
_phoneController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: _C.bg,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new_rounded, color: _C.accent),
onPressed: () => Get.back(),
),
),
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 28),
child: Form(
key: _formKey,
child: Column(
children: [
const Icon(Icons.person_add_rounded, color: _C.accent, size: 64),
const SizedBox(height: 24),
const Text(
'طلب انضمام جديد',
style: TextStyle(
color: _C.textPrimary,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
const Text(
'سيتم مراجعة طلبك من قبل الإدارة',
style: TextStyle(color: _C.textSec, fontSize: 14),
),
const SizedBox(height: 40),
_buildCard(),
const SizedBox(height: 32),
],
),
),
),
),
);
}
Widget _buildCard() {
return Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: _C.card,
borderRadius: BorderRadius.circular(24),
border: Border.all(color: _C.accent.withAlpha(25)), // 0.1 * 255 ≈ 25
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildField('الاسم الكامل', _nameController, Icons.person_outline),
const SizedBox(height: 20),
_buildField('رقم الهاتف', _phoneController, Icons.phone_android, isPhone: true),
const SizedBox(height: 20),
_buildField('كلمة المرور', _passwordController, Icons.lock_outline, isPass: true),
const SizedBox(height: 32),
_isLoading
? const Center(child: CircularProgressIndicator(color: _C.accent))
: ElevatedButton(
onPressed: _register,
style: ElevatedButton.styleFrom(
backgroundColor: _C.accent,
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
child: const Text('إرسال الطلب', style: TextStyle(fontWeight: FontWeight.bold)),
),
],
),
);
}
Widget _buildField(String label, TextEditingController ctrl, IconData icon,
{bool isPhone = false, bool isPass = false}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, color: _C.accent, size: 16),
const SizedBox(width: 8),
Text(label, style: const TextStyle(color: _C.textSec, fontSize: 13)),
],
),
const SizedBox(height: 8),
TextFormField(
controller: ctrl,
obscureText: isPass,
keyboardType: isPhone ? TextInputType.phone : TextInputType.text,
style: const TextStyle(color: _C.textPrimary),
decoration: InputDecoration(
filled: true,
fillColor: _C.inputBg,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
),
validator: (val) => val == null || val.isEmpty ? 'هذا الحقل مطلوب' : null,
),
],
);
}
}