19
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
81
lib/controller/admin/staff_controller.dart
Normal file
81
lib/controller/admin/staff_controller.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
22
lib/debug_jwt.dart
Normal 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!");
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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),
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
195
lib/views/admin/staff/add_staff_page.dart
Normal file
195
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
lib/views/admin/staff/pending_admins_page.dart
Normal file
148
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)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
181
lib/views/auth/register_page.dart
Normal file
181
lib/views/auth/register_page.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user