service 2-5-26-2
This commit is contained in:
@@ -23,15 +23,17 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
super.configureFlutterEngine(flutterEngine)
|
super.configureFlutterEngine(flutterEngine)
|
||||||
|
|
||||||
// Channel for security checks (isRooted)
|
// Channel for security checks (isRooted, getAppSignature)
|
||||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, SECURITY_CHANNEL)
|
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, SECURITY_CHANNEL)
|
||||||
.setMethodCallHandler { call, result ->
|
.setMethodCallHandler { call, result ->
|
||||||
when (call.method) {
|
when (call.method) {
|
||||||
"isNativeRooted" -> result.success(isDeviceCompromised())
|
"isNativeRooted" -> result.success(isDeviceCompromised())
|
||||||
|
"getAppSignature" -> result.success(getAppSignature())
|
||||||
else -> result.notImplemented()
|
else -> result.notImplemented()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Channel for app control (bringing to foreground)
|
// Channel for app control (bringing to foreground)
|
||||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, APP_CONTROL_CHANNEL)
|
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, APP_CONTROL_CHANNEL)
|
||||||
.setMethodCallHandler { call, result ->
|
.setMethodCallHandler { call, result ->
|
||||||
@@ -171,4 +173,35 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
Log.d("MainActivity", "Deleted directory ${dir?.path}: $deleted")
|
Log.d("MainActivity", "Deleted directory ${dir?.path}: $deleted")
|
||||||
return deleted
|
return deleted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getAppSignature(): String? {
|
||||||
|
return try {
|
||||||
|
val packageInfo = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
|
||||||
|
packageManager.getPackageInfo(packageName, android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
packageManager.getPackageInfo(packageName, android.content.pm.PackageManager.GET_SIGNATURES)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val signatures = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
|
||||||
|
packageInfo.signingInfo?.signingCertificateHistory
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
packageInfo.signatures
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signatures != null && signatures.isNotEmpty()) {
|
||||||
|
val signature = signatures[0]
|
||||||
|
val md = java.security.MessageDigest.getInstance("SHA-256")
|
||||||
|
val digest = md.digest(signature.toByteArray())
|
||||||
|
digest.joinToString("") { "%02x".format(it) }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("MainActivity", "Error getting app signature: ${e.message}", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ class BoxName {
|
|||||||
static const String jwt = "jwt";
|
static const String jwt = "jwt";
|
||||||
static const String fingerPrint = "fingerPrint";
|
static const String fingerPrint = "fingerPrint";
|
||||||
static const String deviceFingerprint = "deviceFingerprint";
|
static const String deviceFingerprint = "deviceFingerprint";
|
||||||
|
static const String hmac = "hmac";
|
||||||
|
|
||||||
|
|
||||||
static const String payMobApikey = "payMobApikey";
|
static const String payMobApikey = "payMobApikey";
|
||||||
static const String employeename = "employeename";
|
static const String employeename = "employeename";
|
||||||
|
|||||||
@@ -1,20 +1,25 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class AppColor {
|
class AppColor {
|
||||||
static const Color primaryColor = Color(0xFF1DA1F2);
|
static const Color primaryColor = Color(0xFF2563EB); // Modern Blue
|
||||||
static const Color writeColor = Color(0xff222359);
|
static const Color primaryLight = Color(0xFFDBEAFE);
|
||||||
|
static const Color writeColor = Color(0xFF1E293B); // Darker Slate
|
||||||
|
static const Color surfaceColor = Color(0xFFF8FAFC);
|
||||||
|
|
||||||
static const Color bronze = Color(0xFFCD7F32);
|
static const Color bronze = Color(0xFFCD7F32);
|
||||||
static const Color goldenBronze = Color(0xFFB87333); // Golden bronze color
|
static const Color goldenBronze = Color(0xFFB87333);
|
||||||
static const Color gold = Color(0xFFD4AF37);
|
static const Color gold = Color(0xFFD4AF37);
|
||||||
|
|
||||||
static const Color secondaryColor = Colors.white;
|
static const Color secondaryColor = Colors.white;
|
||||||
static const Color accentColor = Colors.grey;
|
static const Color accentColor = Color(0xFF64748B); // Slate Grey
|
||||||
static const Color twitterColor = Color(0xFF1DA1F2); // Twitter blue
|
static const Color greyColor = Color(0xFF94A3B8);
|
||||||
static const Color greyColor = Colors.grey;
|
|
||||||
static const Color redColor = Color(0xFFEA4335); // Google Red
|
static const Color redColor = Color(0xFFEF4444);
|
||||||
static const Color greenColor = Color(0xFF34A853); // Google Green
|
static const Color greenColor = Color(0xFF10B981);
|
||||||
static const Color blueColor = Color(0xFF1DA1F2); // Google Blue
|
static const Color blueColor = Color(0xFF3B82F6);
|
||||||
static const Color yellowColor = Color(0xFFFBBC05); // Google Yellow
|
static const Color yellowColor = Color(0xFFF59E0B);
|
||||||
static Color deepPurpleAccent =
|
|
||||||
const Color.fromARGB(255, 123, 76, 254).withOpacity(0.3);
|
static Color deepPurpleAccent = const Color(0xFF7C3AED);
|
||||||
|
static Color glassEffect = Colors.white.withOpacity(0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,31 +40,26 @@ class AppStyle {
|
|||||||
color: AppColor.writeColor,
|
color: AppColor.writeColor,
|
||||||
fontFamily: 'digit');
|
fontFamily: 'digit');
|
||||||
|
|
||||||
static BoxDecoration boxDecoration = const BoxDecoration(
|
static BoxDecoration boxDecoration = BoxDecoration(
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: AppColor.accentColor, blurRadius: 5, offset: Offset(2, 4)),
|
color: AppColor.accentColor.withValues(alpha: 0.1),
|
||||||
BoxShadow(
|
blurRadius: 10,
|
||||||
color: AppColor.accentColor, blurRadius: 5, offset: Offset(-2, -2))
|
offset: const Offset(0, 4)),
|
||||||
],
|
],
|
||||||
color: AppColor.secondaryColor,
|
color: AppColor.secondaryColor,
|
||||||
borderRadius: BorderRadius.all(
|
borderRadius: BorderRadius.circular(16));
|
||||||
Radius.elliptical(15, 30),
|
|
||||||
));
|
static BoxDecoration boxDecoration1 = BoxDecoration(
|
||||||
static BoxDecoration boxDecoration1 = const BoxDecoration(
|
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Color.fromARGB(255, 237, 230, 230),
|
color: Colors.black.withValues(alpha: 0.05),
|
||||||
blurRadius: 5,
|
blurRadius: 15,
|
||||||
offset: Offset(2, 4)),
|
offset: const Offset(0, 8)),
|
||||||
BoxShadow(
|
|
||||||
color: Color.fromARGB(255, 242, 237, 237),
|
|
||||||
blurRadius: 5,
|
|
||||||
offset: Offset(-2, -2))
|
|
||||||
],
|
],
|
||||||
color: AppColor.secondaryColor,
|
color: AppColor.secondaryColor,
|
||||||
borderRadius: BorderRadius.all(
|
borderRadius: BorderRadius.circular(20),
|
||||||
Radius.elliptical(15, 30),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,17 @@ import '../../constant/box_name.dart';
|
|||||||
import '../../main.dart';
|
import '../../main.dart';
|
||||||
import '../../print.dart';
|
import '../../print.dart';
|
||||||
|
|
||||||
|
@pragma('vm:entry-point')
|
||||||
|
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||||
|
// If you're going to use other Firebase services in the background, such as Firestore,
|
||||||
|
// make sure you call `initializeApp` before using other Firebase services.
|
||||||
|
Log.print("Handling a background message: ${message.messageId}");
|
||||||
|
|
||||||
|
if (message.data.isNotEmpty && message.notification != null) {
|
||||||
|
// في وضع الخلفية، يفضل إرسال إشعار محلي أو تحديث البيانات الصامتة
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class FirebaseMessagesController extends GetxController {
|
class FirebaseMessagesController extends GetxController {
|
||||||
final fcmToken = FirebaseMessaging.instance;
|
final fcmToken = FirebaseMessaging.instance;
|
||||||
|
|
||||||
@@ -60,24 +71,18 @@ class FirebaseMessagesController extends GetxController {
|
|||||||
});
|
});
|
||||||
// 🔹 الاشتراك في topic
|
// 🔹 الاشتراك في topic
|
||||||
await fcmToken.subscribeToTopic("service"); // أو "users" حسب نوع المستخدم
|
await fcmToken.subscribeToTopic("service"); // أو "users" حسب نوع المستخدم
|
||||||
print("Subscribed to 'service' topic ✅");
|
Log.print("Subscribed to 'service' topic ✅");
|
||||||
|
|
||||||
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
|
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
|
||||||
// If the app is in the background or terminated, show a system tray message
|
// If the app is in the background or terminated, show a system tray message
|
||||||
RemoteNotification? notification = message.notification;
|
|
||||||
AndroidNotification? android = notification?.android;
|
|
||||||
// if (notification != null && android != null) {
|
|
||||||
if (message.data.isNotEmpty && message.notification != null) {
|
|
||||||
fireBaseTitles(message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
FirebaseMessaging.onBackgroundMessage((RemoteMessage message) async {
|
|
||||||
// Handle background message
|
|
||||||
if (message.data.isNotEmpty && message.notification != null) {
|
if (message.data.isNotEmpty && message.notification != null) {
|
||||||
fireBaseTitles(message);
|
fireBaseTitles(message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// استخدام الدالة العامة للهاندلر في الخلفية
|
||||||
|
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
|
||||||
|
|
||||||
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
|
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
|
||||||
if (message.data.isNotEmpty && message.notification != null) {
|
if (message.data.isNotEmpty && message.notification != null) {
|
||||||
fireBaseTitles(message);
|
fireBaseTitles(message);
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:service/constant/box_name.dart';
|
import 'package:service/constant/box_name.dart';
|
||||||
import 'package:service/constant/links.dart';
|
import 'package:service/constant/links.dart';
|
||||||
import 'package:service/controller/functions/encrypt_decrypt.dart';
|
import 'package:service/controller/functions/encrypt_decrypt.dart';
|
||||||
import 'package:service/env/env.dart';
|
import 'package:service/env/env.dart';
|
||||||
|
import 'package:service/controller/functions/security_helper.dart';
|
||||||
import 'package:service/main.dart';
|
import 'package:service/main.dart';
|
||||||
import 'package:service/print.dart';
|
import 'package:service/print.dart';
|
||||||
|
|
||||||
@@ -15,6 +17,8 @@ import '../../constant/api_key.dart';
|
|||||||
|
|
||||||
class CRUD {
|
class CRUD {
|
||||||
static bool _isRefreshingJWT = false;
|
static bool _isRefreshingJWT = false;
|
||||||
|
static String? _appSignature;
|
||||||
|
|
||||||
static String _lastErrorSignature = '';
|
static String _lastErrorSignature = '';
|
||||||
static DateTime _lastErrorTimestamp = DateTime(2000);
|
static DateTime _lastErrorTimestamp = DateTime(2000);
|
||||||
static const Duration _errorLogDebounceDuration = Duration(minutes: 1);
|
static const Duration _errorLogDebounceDuration = Duration(minutes: 1);
|
||||||
@@ -81,6 +85,22 @@ class CRUD {
|
|||||||
return box.read(BoxName.fingerPrint)?.toString() ?? '';
|
return box.read(BoxName.fingerPrint)?.toString() ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _generateHmac(String body, String timestamp, String nonce) {
|
||||||
|
// نستخدم المفتاح الخاص بالمستخدم (المخزن في البوكس) كـ HMAC Secret
|
||||||
|
final hmacSecret = box.read(BoxName.hmac) ?? '';
|
||||||
|
|
||||||
|
final payload = body + timestamp + nonce;
|
||||||
|
final key = utf8.encode(hmacSecret);
|
||||||
|
final bytes = utf8.encode(payload);
|
||||||
|
final hmacSha256 = Hmac(sha256, key);
|
||||||
|
final result = hmacSha256.convert(bytes).toString();
|
||||||
|
Log.print('🔐 [HMAC-DEBUG] Secret: $hmacSecret');
|
||||||
|
Log.print('🔐 [HMAC-DEBUG] Body(${body.length}): "$body"');
|
||||||
|
Log.print('🔐 [HMAC-DEBUG] TS: $timestamp | Nonce: $nonce');
|
||||||
|
Log.print('🔐 [HMAC-DEBUG] Result: $result');
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
// _makeRequest — Central Request Handler
|
// _makeRequest — Central Request Handler
|
||||||
// ───────────────────────────────────────────────────────────────
|
// ───────────────────────────────────────────────────────────────
|
||||||
@@ -91,6 +111,28 @@ class CRUD {
|
|||||||
}) async {
|
}) async {
|
||||||
const totalTimeout = Duration(seconds: 60);
|
const totalTimeout = Duration(seconds: 60);
|
||||||
|
|
||||||
|
// توليد بيانات الـ HMAC للطلب الحالي
|
||||||
|
final timestamp = DateTime.now().millisecondsSinceEpoch.toString();
|
||||||
|
final nonce =
|
||||||
|
DateTime.now().microsecondsSinceEpoch.toString(); // Nonce فريد
|
||||||
|
|
||||||
|
// تحويل الـ payload إلى string لمحاكة ما سيصل للسيرفر (php://input)
|
||||||
|
String bodyString = '';
|
||||||
|
if (payload != null && payload.isNotEmpty) {
|
||||||
|
// الـ http.post يرسل البيانات كـ x-www-form-urlencoded
|
||||||
|
bodyString = payload.keys
|
||||||
|
.map((key) =>
|
||||||
|
"$key=${Uri.encodeQueryComponent(payload[key].toString())}")
|
||||||
|
.join("&");
|
||||||
|
}
|
||||||
|
|
||||||
|
final hmacSignature = _generateHmac(bodyString, timestamp, nonce);
|
||||||
|
|
||||||
|
// إضافة هيدرات الـ HMAC
|
||||||
|
headers['X-HMAC-Auth'] = hmacSignature;
|
||||||
|
headers['X-Timestamp'] = timestamp;
|
||||||
|
headers['X-Nonce'] = nonce;
|
||||||
|
|
||||||
Future<http.Response> doPost() {
|
Future<http.Response> doPost() {
|
||||||
final url = Uri.parse(link);
|
final url = Uri.parse(link);
|
||||||
return http
|
return http
|
||||||
@@ -105,9 +147,9 @@ class CRUD {
|
|||||||
|
|
||||||
Log.print('🚀 [REQ-$requestId] $link');
|
Log.print('🚀 [REQ-$requestId] $link');
|
||||||
Log.print('🔑 [FP-$requestId] ${headers['X-Device-FP']}');
|
Log.print('🔑 [FP-$requestId] ${headers['X-Device-FP']}');
|
||||||
|
Log.print('🔏 [SIGN-$requestId] ${headers['X-App-Signature']}');
|
||||||
if (payload != null) Log.print('📦 [PAYLOAD-$requestId] $payload');
|
if (payload != null) Log.print('📦 [PAYLOAD-$requestId] $payload');
|
||||||
|
|
||||||
|
|
||||||
while (attempts < 3) {
|
while (attempts < 3) {
|
||||||
try {
|
try {
|
||||||
attempts++;
|
attempts++;
|
||||||
@@ -190,10 +232,14 @@ class CRUD {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize app signature if null
|
||||||
|
_appSignature ??= await SecurityHelper.getAppSignature();
|
||||||
|
|
||||||
final headers = {
|
final headers = {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
'Authorization': 'Bearer $token',
|
'Authorization': 'Bearer $token',
|
||||||
'X-Device-FP': _getFpHeader(),
|
'X-Device-FP': _getFpHeader(),
|
||||||
|
'X-App-Signature': _appSignature ?? '',
|
||||||
};
|
};
|
||||||
|
|
||||||
return await _makeRequest(link: link, payload: payload, headers: headers);
|
return await _makeRequest(link: link, payload: payload, headers: headers);
|
||||||
@@ -219,8 +265,12 @@ class CRUD {
|
|||||||
'aud': 'service',
|
'aud': 'service',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Initialize app signature if null
|
||||||
|
_appSignature ??= await SecurityHelper.getAppSignature();
|
||||||
|
|
||||||
final headers = {
|
final headers = {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-App-Signature': _appSignature ?? '',
|
||||||
};
|
};
|
||||||
|
|
||||||
final response = await _makeRequest(
|
final response = await _makeRequest(
|
||||||
@@ -230,8 +280,17 @@ class CRUD {
|
|||||||
response is Map &&
|
response is Map &&
|
||||||
response['status'] == 'success') {
|
response['status'] == 'success') {
|
||||||
final jwt = response['message']['jwt'];
|
final jwt = response['message']['jwt'];
|
||||||
|
final hmac = response['message']['hmac'];
|
||||||
|
|
||||||
Log.print('jwt: $jwt');
|
Log.print('jwt: $jwt');
|
||||||
box.write(BoxName.jwt, c(jwt));
|
Log.print('hmac_key: $hmac');
|
||||||
|
|
||||||
|
await box.write(BoxName.jwt, c(jwt));
|
||||||
|
if (hmac != null) {
|
||||||
|
await box.write(BoxName.hmac, hmac);
|
||||||
|
final verify = box.read(BoxName.hmac);
|
||||||
|
Log.print('✅ Verified stored HMAC: $verify');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
32
lib/controller/functions/security_helper.dart
Normal file
32
lib/controller/functions/security_helper.dart
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:service/print.dart';
|
||||||
|
|
||||||
|
class SecurityHelper {
|
||||||
|
static const platform = MethodChannel('com.service_intaleq/security');
|
||||||
|
|
||||||
|
static Future<String?> getAppSignature() async {
|
||||||
|
try {
|
||||||
|
final String? signature = await platform.invokeMethod('getAppSignature');
|
||||||
|
final mode = kDebugMode ? 'DEBUG' : 'RELEASE';
|
||||||
|
Log.print('----------------------------------------------------');
|
||||||
|
Log.print('🚀 APP SIGNATURE HASH ($mode): $signature');
|
||||||
|
Log.print('----------------------------------------------------');
|
||||||
|
return signature;
|
||||||
|
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
Log.print('❌ Failed to get app signature: ${e.message}');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<bool> isDeviceRooted() async {
|
||||||
|
try {
|
||||||
|
final bool isRooted = await platform.invokeMethod('isNativeRooted');
|
||||||
|
return isRooted;
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
Log.print('❌ Failed to check root: ${e.message}');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,11 +43,16 @@ class LoginController extends GetxController {
|
|||||||
Log.print('📥 Login Response: $res');
|
Log.print('📥 Login Response: $res');
|
||||||
|
|
||||||
if (res != 'failure' && res is Map && res['status'] == 'success') {
|
if (res != 'failure' && res is Map && res['status'] == 'success') {
|
||||||
var d = res['message']; // V1 returns {status, message: {jwt, data: {user...}}}
|
var d = res[
|
||||||
|
'message']; // V1 returns {status, message: {jwt, data: {user...}}}
|
||||||
|
|
||||||
// Store JWT
|
// Store JWT & HMAC
|
||||||
final jwt = d['jwt'];
|
final jwt = d['jwt'];
|
||||||
box.write(BoxName.jwt, c(jwt));
|
final hmac = d['hmac'];
|
||||||
|
await box.write(BoxName.jwt, c(jwt));
|
||||||
|
if (hmac != null) {
|
||||||
|
await box.write(BoxName.hmac, hmac);
|
||||||
|
}
|
||||||
|
|
||||||
// Store User Data
|
// Store User Data
|
||||||
var userData = d['data'];
|
var userData = d['data'];
|
||||||
@@ -79,7 +84,6 @@ class LoginController extends GetxController {
|
|||||||
login();
|
login();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
super.onInit();
|
super.onInit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:service/constant/box_name.dart';
|
|
||||||
import 'package:service/constant/colors.dart';
|
import 'package:service/constant/colors.dart';
|
||||||
import 'package:service/constant/links.dart';
|
import 'package:service/constant/links.dart';
|
||||||
import 'package:service/controller/functions/crud.dart';
|
import 'package:service/controller/functions/crud.dart';
|
||||||
@@ -49,10 +46,41 @@ class MainController extends GetxController {
|
|||||||
var color = ''.obs;
|
var color = ''.obs;
|
||||||
var colorHex = ''.obs;
|
var colorHex = ''.obs;
|
||||||
|
|
||||||
searchPassengerByPhone() async {
|
@override
|
||||||
if (formKey.currentState!.validate()) {
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
// refreshDashboardStats(); // Removed to save data consumption at start
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refreshDashboardStats() async {
|
||||||
|
isLoading = true;
|
||||||
|
update();
|
||||||
|
try {
|
||||||
|
await Future.wait<void>([
|
||||||
|
getDriverWantCompleteRegistration(),
|
||||||
|
getDriverNotCompleteRegistration(),
|
||||||
|
getNewDriverRegister(),
|
||||||
|
]);
|
||||||
|
} catch (e) {
|
||||||
|
Log.print('Error refreshing stats: $e');
|
||||||
|
}
|
||||||
|
isLoading = false;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> searchPassengerByPhone() async {
|
||||||
|
if (formKey.currentState == null || formKey.currentState!.validate()) {
|
||||||
|
isLoading = true;
|
||||||
|
update();
|
||||||
await getPassengersByPhone();
|
await getPassengersByPhone();
|
||||||
|
isLoading = false;
|
||||||
|
update();
|
||||||
Get.back();
|
Get.back();
|
||||||
|
if (passengerData.isEmpty) {
|
||||||
|
Get.snackbar('Error'.tr, 'Passenger not found'.tr,
|
||||||
|
backgroundColor: Colors.red, colorText: Colors.white);
|
||||||
|
return;
|
||||||
|
}
|
||||||
Get.to(() => PassengersPage());
|
Get.to(() => PassengersPage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,7 +193,6 @@ class MainController extends GetxController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uri != null) {
|
|
||||||
final ok = await canLaunchUrl(uri);
|
final ok = await canLaunchUrl(uri);
|
||||||
if (ok) {
|
if (ok) {
|
||||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||||
@@ -173,10 +200,9 @@ class MainController extends GetxController {
|
|||||||
// ممكن تضيف Snackbar/Toast هنا
|
// ممكن تضيف Snackbar/Toast هنا
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
List driverNotCompleteRegistration = [];
|
List driverNotCompleteRegistration = [];
|
||||||
getDriverNotCompleteRegistration() async {
|
Future<void> getDriverNotCompleteRegistration() async {
|
||||||
var res = await CRUD()
|
var res = await CRUD()
|
||||||
.get(link: AppLink.getDriverNotCompleteRegistration, payload: {});
|
.get(link: AppLink.getDriverNotCompleteRegistration, payload: {});
|
||||||
if (res != 'failure') {
|
if (res != 'failure') {
|
||||||
@@ -185,11 +211,12 @@ class MainController extends GetxController {
|
|||||||
filteredDrivers = driverNotCompleteRegistration;
|
filteredDrivers = driverNotCompleteRegistration;
|
||||||
update();
|
update();
|
||||||
} else {
|
} else {
|
||||||
Get.snackbar(res, '');
|
driverNotCompleteRegistration = [];
|
||||||
|
update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteDriverNotCompleteRegistration(String phone) async {
|
Future<void> deleteDriverNotCompleteRegistration(String phone) async {
|
||||||
var res = await CRUD()
|
var res = await CRUD()
|
||||||
.get(link: AppLink.deleteDriverNotCompleteRegistration, payload: {
|
.get(link: AppLink.deleteDriverNotCompleteRegistration, payload: {
|
||||||
'phone': phone,
|
'phone': phone,
|
||||||
@@ -204,7 +231,7 @@ class MainController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List driverWantCompleteRegistration = [];
|
List driverWantCompleteRegistration = [];
|
||||||
getDriverWantCompleteRegistration() async {
|
Future<void> getDriverWantCompleteRegistration() async {
|
||||||
var res =
|
var res =
|
||||||
await CRUD().get(link: AppLink.getDriversWaitingActive, payload: {});
|
await CRUD().get(link: AppLink.getDriversWaitingActive, payload: {});
|
||||||
if (res != 'failure') {
|
if (res != 'failure') {
|
||||||
@@ -213,12 +240,13 @@ class MainController extends GetxController {
|
|||||||
filteredDrivers = driverWantCompleteRegistration;
|
filteredDrivers = driverWantCompleteRegistration;
|
||||||
update();
|
update();
|
||||||
} else {
|
} else {
|
||||||
Get.snackbar(res, '');
|
driverWantCompleteRegistration = [];
|
||||||
|
update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List driversPhoneNotComplete = [];
|
List driversPhoneNotComplete = [];
|
||||||
getDriversPhoneNotComplete() async {
|
Future<void> getDriversPhoneNotComplete() async {
|
||||||
var res =
|
var res =
|
||||||
await CRUD().get(link: AppLink.getDriversPhoneNotComplete, payload: {});
|
await CRUD().get(link: AppLink.getDriversPhoneNotComplete, payload: {});
|
||||||
if (res != 'failure') {
|
if (res != 'failure') {
|
||||||
@@ -232,18 +260,19 @@ class MainController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List newDriverRegister = [];
|
List newDriverRegister = [];
|
||||||
getNewDriverRegister() async {
|
Future<void> getNewDriverRegister() async {
|
||||||
var res = await CRUD().get(link: AppLink.getNewDriverRegister, payload: {});
|
var res = await CRUD().get(link: AppLink.getNewDriverRegister, payload: {});
|
||||||
if (res != 'failure') {
|
if (res != 'failure') {
|
||||||
var d = res['message'];
|
var d = res['message'];
|
||||||
newDriverRegister = d;
|
newDriverRegister = d;
|
||||||
update();
|
update();
|
||||||
} else {
|
} else {
|
||||||
Get.snackbar(res, '');
|
newDriverRegister = [];
|
||||||
|
update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addWelcomeCall(String driveId) async {
|
Future<void> addWelcomeCall(String driveId) async {
|
||||||
var res = await CRUD().post(link: AppLink.addWelcomeDriverNote, payload: {
|
var res = await CRUD().post(link: AppLink.addWelcomeDriverNote, payload: {
|
||||||
"driverId": driveId,
|
"driverId": driveId,
|
||||||
"notes": notesController.text,
|
"notes": notesController.text,
|
||||||
@@ -255,7 +284,7 @@ class MainController extends GetxController {
|
|||||||
|
|
||||||
String selectedStatus = "I'm not ready yet".tr;
|
String selectedStatus = "I'm not ready yet".tr;
|
||||||
List passengerNotCompleteRegistration = [];
|
List passengerNotCompleteRegistration = [];
|
||||||
getPassengerNotCompleteRegistration() async {
|
Future<void> getPassengerNotCompleteRegistration() async {
|
||||||
var res = await CRUD()
|
var res = await CRUD()
|
||||||
.get(link: AppLink.getPassengersNotCompleteRegistration, payload: {});
|
.get(link: AppLink.getPassengersNotCompleteRegistration, payload: {});
|
||||||
if (res != 'failure') {
|
if (res != 'failure') {
|
||||||
@@ -463,11 +492,16 @@ class MainController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
searchDriverByPhone() async {
|
searchDriverByPhone() async {
|
||||||
if (formKey.currentState!.validate()) {
|
if (formKey.currentState == null || formKey.currentState!.validate()) {
|
||||||
|
isLoading = true;
|
||||||
|
update();
|
||||||
await getDriverByPhone();
|
await getDriverByPhone();
|
||||||
|
isLoading = false;
|
||||||
|
update();
|
||||||
Get.back();
|
Get.back();
|
||||||
if (driverData.isEmpty) {
|
if (driverData.isEmpty) {
|
||||||
Get.snackbar('Error', 'Driver not found', backgroundColor: Colors.red);
|
Get.snackbar('Error'.tr, 'Driver not found'.tr,
|
||||||
|
backgroundColor: Colors.red, colorText: Colors.white);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Get.to(() => DriverPage());
|
Get.to(() => DriverPage());
|
||||||
@@ -475,11 +509,16 @@ class MainController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
searchDriverByNational() async {
|
searchDriverByNational() async {
|
||||||
if (formKey.currentState!.validate()) {
|
if (formKey.currentState == null || formKey.currentState!.validate()) {
|
||||||
|
isLoading = true;
|
||||||
|
update();
|
||||||
await getDriverByNational();
|
await getDriverByNational();
|
||||||
|
isLoading = false;
|
||||||
|
update();
|
||||||
Get.back();
|
Get.back();
|
||||||
if (driverData.isEmpty) {
|
if (driverData.isEmpty) {
|
||||||
Get.snackbar('Error', 'Driver not found', backgroundColor: Colors.red);
|
Get.snackbar('Error'.tr, 'Driver not found'.tr,
|
||||||
|
backgroundColor: Colors.red, colorText: Colors.white);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Get.to(() => DriverPage());
|
Get.to(() => DriverPage());
|
||||||
|
|||||||
@@ -24,10 +24,12 @@ class DriverPage extends StatelessWidget {
|
|||||||
child: ListView(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
_buildDriverInfoSection(data),
|
_buildDriverInfoSection(data),
|
||||||
|
_buildCommunicationSection(data, context),
|
||||||
_buildStatisticsSection(data),
|
_buildStatisticsSection(data),
|
||||||
_buildCarInfoSection(data),
|
_buildCarInfoSection(data),
|
||||||
_buildLicenseInfoSection(data),
|
_buildLicenseInfoSection(data),
|
||||||
_buildBankInfoSection(data),
|
_buildBankInfoSection(data),
|
||||||
|
const SizedBox(height: 40),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -175,4 +177,50 @@ class DriverPage extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildCommunicationSection(Map data, BuildContext context) {
|
||||||
|
String phone = data['phone'] ?? '';
|
||||||
|
String name = data['first_name'] ?? '';
|
||||||
|
|
||||||
|
return CupertinoListSection.insetGrouped(
|
||||||
|
header: Text('Quick Communication'.tr),
|
||||||
|
children: [
|
||||||
|
CupertinoListTile(
|
||||||
|
title: Text('Call Driver'.tr),
|
||||||
|
leading: const Icon(CupertinoIcons.phone_fill, color: Colors.green),
|
||||||
|
onTap: () => mainController.makePhoneCall(phone),
|
||||||
|
),
|
||||||
|
CupertinoListTile(
|
||||||
|
title: Text('WhatsApp: Activation'.tr),
|
||||||
|
leading: const Icon(Icons.send, color: Colors.green),
|
||||||
|
onTap: () => mainController.launchCommunication(
|
||||||
|
'whatsapp',
|
||||||
|
phone,
|
||||||
|
'أهلاً بك يا كابتن $name في انطلق! تم تفعيل حسابك بنجاح وأصبحت مستعداً لاستقبال الرحلات.',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
CupertinoListTile(
|
||||||
|
title: Text('WhatsApp: Missing Docs'.tr),
|
||||||
|
leading: const Icon(Icons.send, color: Colors.orange),
|
||||||
|
onTap: () => mainController.launchCommunication(
|
||||||
|
'whatsapp',
|
||||||
|
phone,
|
||||||
|
'مرحباً كابتن $name، يرجى تزويدنا بالأوراق الناقصة أو غير الواضحة عبر الواتساب لإكمال تفعيل حسابك.',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
CupertinoListTile(
|
||||||
|
title: Text('WhatsApp: Support'.tr),
|
||||||
|
leading: const Icon(Icons.send, color: Colors.blue),
|
||||||
|
onTap: () => mainController.launchCommunication(
|
||||||
|
'whatsapp',
|
||||||
|
phone,
|
||||||
|
'مرحباً كابتن $name، معك الدعم الفني من شركة انطلق. كيف يمكنني مساعدتك اليوم؟',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ const storage = FlutterSecureStorage();
|
|||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
await GetStorage.init();
|
||||||
|
|
||||||
await EncryptionHelper.initialize();
|
await EncryptionHelper.initialize();
|
||||||
|
|
||||||
if (Firebase.apps.isEmpty) {
|
if (Firebase.apps.isEmpty) {
|
||||||
await Firebase.initializeApp(
|
await Firebase.initializeApp(
|
||||||
options: DefaultFirebaseOptions.currentPlatform);
|
options: DefaultFirebaseOptions.currentPlatform);
|
||||||
|
|||||||
@@ -12,28 +12,14 @@ import 'package:service/views/widgets/elevated_btn.dart';
|
|||||||
import 'package:service/views/widgets/my_dialog.dart';
|
import 'package:service/views/widgets/my_dialog.dart';
|
||||||
import 'package:service/views/widgets/my_textField.dart';
|
import 'package:service/views/widgets/my_textField.dart';
|
||||||
|
|
||||||
import '../../constant/box_name.dart';
|
|
||||||
import '../../constant/style.dart';
|
import '../../constant/style.dart';
|
||||||
import '../../controller/mainController/pages/add_car.dart';
|
import '../../controller/mainController/pages/add_car.dart';
|
||||||
import '../../controller/mainController/pages/drivers_cant_register.dart';
|
import '../../controller/mainController/pages/drivers_cant_register.dart';
|
||||||
import '../../controller/mainController/pages/new_driver.dart';
|
import '../../controller/mainController/pages/new_driver.dart';
|
||||||
import '../../controller/mainController/pages/welcome_call.dart';
|
import '../../controller/mainController/pages/welcome_call.dart';
|
||||||
import '../../main.dart';
|
import '../../main.dart';
|
||||||
import '../../print.dart';
|
|
||||||
import '../widgets/my_scafold.dart';
|
import '../widgets/my_scafold.dart';
|
||||||
|
|
||||||
// --- Service Item Model ---
|
|
||||||
// A helper class to structure the data for each service card.
|
|
||||||
// This makes the code cleaner and easier to manage.
|
|
||||||
class ServiceItem {
|
|
||||||
final String title;
|
|
||||||
final IconData icon;
|
|
||||||
final VoidCallback onTap;
|
|
||||||
|
|
||||||
ServiceItem({required this.title, required this.icon, required this.onTap});
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Main Screen Widget (Redesigned) ---
|
|
||||||
class Main extends StatelessWidget {
|
class Main extends StatelessWidget {
|
||||||
Main({super.key});
|
Main({super.key});
|
||||||
|
|
||||||
@@ -41,103 +27,71 @@ class Main extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// --- List of Services ---
|
return MyScaffold(title: 'Intaleq Service'.tr, isleading: false, body: [
|
||||||
// All services are defined here in a list. This makes it easy to add, remove, or reorder them.
|
Container(
|
||||||
// The original onTap logic is preserved exactly as it was.
|
color: AppColor.surfaceColor,
|
||||||
final List<ServiceItem> services = [
|
child: SingleChildScrollView(
|
||||||
|
physics: const BouncingScrollPhysics(),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// --- Header Section ---
|
||||||
|
_buildHeader(),
|
||||||
|
|
||||||
|
// --- Statistics Section ---
|
||||||
|
_buildStatsSection(),
|
||||||
|
|
||||||
|
// --- Actions Section ---
|
||||||
|
_buildCategoryTitle('🔍 Search & Inquiries'.tr),
|
||||||
|
_buildGridSection([
|
||||||
ServiceItem(
|
ServiceItem(
|
||||||
title: 'passenger details by phone'.tr,
|
title: 'passenger details by phone'.tr,
|
||||||
icon: Icons.person_search_rounded,
|
icon: Icons.person_search_rounded,
|
||||||
onTap: () {
|
color: Colors.blue,
|
||||||
MyDialog().getDialog(
|
onTap: () => _showSearchDialog(
|
||||||
'insert passenger phone'.tr,
|
title: 'insert passenger phone'.tr,
|
||||||
'midTitle',
|
|
||||||
Column(children: [
|
|
||||||
Form(
|
|
||||||
key: mainController.formKey,
|
|
||||||
child: MyTextForm(
|
|
||||||
controller: mainController.passengerPhoneController,
|
controller: mainController.passengerPhoneController,
|
||||||
label: 'insert passenger phone'.tr,
|
onSearch: () => mainController.searchPassengerByPhone(),
|
||||||
hint: 'insert passenger phone'.tr,
|
|
||||||
type: TextInputType.phone,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]),
|
|
||||||
() => mainController.searchPassengerByPhone(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ServiceItem(
|
ServiceItem(
|
||||||
title: 'Driver details by phone'.tr,
|
title: 'Driver details by phone'.tr,
|
||||||
icon: Icons.support_agent_rounded,
|
icon: Icons.contact_phone_rounded,
|
||||||
onTap: () {
|
color: Colors.indigo,
|
||||||
MyDialog().getDialog(
|
onTap: () => _showSearchDialog(
|
||||||
'insert Driver phone'.tr,
|
title: 'insert Driver phone'.tr,
|
||||||
'midTitle',
|
|
||||||
Column(children: [
|
|
||||||
Form(
|
|
||||||
key: mainController.formKey,
|
|
||||||
child: MyTextForm(
|
|
||||||
controller: mainController.driverPhoneController,
|
controller: mainController.driverPhoneController,
|
||||||
label: 'insert Driver phone'.tr,
|
onSearch: () => mainController.searchDriverByPhone(),
|
||||||
hint: 'insert Driver phone'.tr,
|
|
||||||
type: TextInputType.phone,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]),
|
|
||||||
() => mainController.searchDriverByPhone(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ServiceItem(
|
ServiceItem(
|
||||||
title: 'Driver details by national number'.tr,
|
title: 'Driver details by national number'.tr,
|
||||||
icon: Icons.support_agent_rounded,
|
icon: Icons.badge_rounded,
|
||||||
onTap: () {
|
color: Colors.teal,
|
||||||
MyDialog().getDialog(
|
onTap: () => _showSearchDialog(
|
||||||
'insert Driver national'.tr,
|
title: 'insert Driver national'.tr,
|
||||||
'midTitle',
|
|
||||||
Column(children: [
|
|
||||||
Form(
|
|
||||||
key: mainController.formKey,
|
|
||||||
child: MyTextForm(
|
|
||||||
controller: mainController.driverPhoneController,
|
controller: mainController.driverPhoneController,
|
||||||
label: 'insert Driver national'.tr,
|
|
||||||
hint: 'insert Driver national'.tr,
|
|
||||||
type: TextInputType.number,
|
type: TextInputType.number,
|
||||||
|
onSearch: () => mainController.searchDriverByNational(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
() => mainController.searchDriverByNational(),
|
|
||||||
);
|
_buildCategoryTitle('⏳ Approval Queue'.tr),
|
||||||
},
|
_buildGridSection([
|
||||||
),
|
|
||||||
ServiceItem(
|
ServiceItem(
|
||||||
title: 'Drivers waitting Register'.tr,
|
title: 'Drivers waitting Register'.tr,
|
||||||
icon: Icons.pending_actions_rounded,
|
icon: Icons.pending_actions_rounded,
|
||||||
|
color: Colors.orange,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await mainController.getDriverWantCompleteRegistration();
|
await mainController.getDriverWantCompleteRegistration();
|
||||||
Get.to(() => DriversCantRegister());
|
Get.to(() => DriversCantRegister());
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ServiceItem(
|
|
||||||
title: 'Register new driver'.tr,
|
|
||||||
icon: Icons.person,
|
|
||||||
onTap: () {
|
|
||||||
// await mainController.getDriverWantCompleteRegistration();
|
|
||||||
Get.to(() => RegisterCaptain());
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ServiceItem(
|
|
||||||
title: 'Drivers Cant Register'.tr,
|
|
||||||
icon: Icons.car_crash,
|
|
||||||
onTap: () async {
|
|
||||||
await mainController.getDriverNotCompleteRegistration();
|
|
||||||
Get.to(() => DriversCantRegister());
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ServiceItem(
|
ServiceItem(
|
||||||
title: 'Drivers phones not register'.tr,
|
title: 'Drivers phones not register'.tr,
|
||||||
icon: Icons.person,
|
icon: Icons.phone_disabled_rounded,
|
||||||
|
color: Colors.blueGrey,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await mainController.getDriversPhoneNotComplete();
|
await mainController.getDriversPhoneNotComplete();
|
||||||
Get.to(() => DriversCantRegister());
|
Get.to(() => DriversCantRegister());
|
||||||
@@ -145,23 +99,61 @@ class Main extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
ServiceItem(
|
ServiceItem(
|
||||||
title: 'Drivers Activity'.tr,
|
title: 'Drivers Activity'.tr,
|
||||||
icon: Icons.person,
|
icon: Icons.assessment_rounded,
|
||||||
|
color: Colors.blueAccent,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await mainController.getDriversPhoneNotComplete();
|
await mainController.getDriversPhoneNotComplete();
|
||||||
Get.to(() => DriversCantRegister());
|
Get.to(() => DriversCantRegister());
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
ServiceItem(
|
||||||
|
title: 'Drivers Cant Register'.tr,
|
||||||
|
icon: Icons.car_crash_rounded,
|
||||||
|
color: Colors.redAccent,
|
||||||
|
onTap: () async {
|
||||||
|
await mainController.getDriverNotCompleteRegistration();
|
||||||
|
Get.to(() => DriversCantRegister());
|
||||||
|
},
|
||||||
|
),
|
||||||
ServiceItem(
|
ServiceItem(
|
||||||
title: 'Passengers Cant Register'.tr,
|
title: 'Passengers Cant Register'.tr,
|
||||||
icon: Icons.group_off_rounded,
|
icon: Icons.group_off_rounded,
|
||||||
|
color: Colors.deepOrange,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await mainController.getPassengerNotCompleteRegistration();
|
await mainController.getPassengerNotCompleteRegistration();
|
||||||
Get.to(() => PassengersCantRegister());
|
Get.to(() => PassengersCantRegister());
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
]),
|
||||||
|
|
||||||
|
_buildCategoryTitle('⚡ Quick Actions'.tr),
|
||||||
|
_buildGridSection([
|
||||||
|
ServiceItem(
|
||||||
|
title: 'Register new driver'.tr,
|
||||||
|
icon: Icons.person_add_rounded,
|
||||||
|
color: Colors.green,
|
||||||
|
onTap: () => Get.to(() => RegisterCaptain()),
|
||||||
|
),
|
||||||
|
ServiceItem(
|
||||||
|
title: 'Add Driver Who Wants to Work'.tr,
|
||||||
|
icon: Icons.handshake_rounded,
|
||||||
|
color: Colors.lightGreen,
|
||||||
|
onTap: () => _showAddDriverDialog(),
|
||||||
|
),
|
||||||
|
ServiceItem(
|
||||||
|
title: 'Add Car Who Wants to Work'.tr,
|
||||||
|
icon: Icons.add_road_rounded,
|
||||||
|
color: Colors.cyan,
|
||||||
|
onTap: () => _showAddCarDialog(),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
|
||||||
|
_buildCategoryTitle('🚗 Vehicle Management'.tr),
|
||||||
|
_buildGridSection([
|
||||||
ServiceItem(
|
ServiceItem(
|
||||||
title: 'Add car'.tr,
|
title: 'Add car'.tr,
|
||||||
icon: Icons.add_circle_outline_rounded,
|
icon: Icons.add_circle_outline_rounded,
|
||||||
|
color: Colors.purple,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await mainController.getdriverWithoutCar();
|
await mainController.getdriverWithoutCar();
|
||||||
if (mainController.driverWithoutCar.isNotEmpty) {
|
if (mainController.driverWithoutCar.isNotEmpty) {
|
||||||
@@ -171,7 +163,8 @@ class Main extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
ServiceItem(
|
ServiceItem(
|
||||||
title: 'Edit car plate'.tr,
|
title: 'Edit car plate'.tr,
|
||||||
icon: Icons.edit_note_rounded,
|
icon: Icons.edit_attributes_rounded,
|
||||||
|
color: Colors.amber,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await mainController.getCarPlateNotEdit();
|
await mainController.getCarPlateNotEdit();
|
||||||
if (mainController.carPlateNotEdit.isNotEmpty) {
|
if (mainController.carPlateNotEdit.isNotEmpty) {
|
||||||
@@ -179,16 +172,20 @@ class Main extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
]),
|
||||||
|
|
||||||
|
_buildCategoryTitle('📊 Reporting & Quality'.tr),
|
||||||
|
_buildGridSection([
|
||||||
ServiceItem(
|
ServiceItem(
|
||||||
title: "View complaint".tr,
|
title: "View complaint".tr,
|
||||||
icon: Icons.report_problem_rounded,
|
icon: Icons.report_problem_rounded,
|
||||||
onTap: () {
|
color: Colors.deepOrange,
|
||||||
Get.to(() => const Complaint());
|
onTap: () => Get.to(() => const Complaint()),
|
||||||
},
|
|
||||||
),
|
),
|
||||||
ServiceItem(
|
ServiceItem(
|
||||||
title: "Welcome call".tr,
|
title: "Welcome call".tr,
|
||||||
icon: Icons.ring_volume_rounded,
|
icon: Icons.ring_volume_rounded,
|
||||||
|
color: Colors.pink,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await mainController.getNewDriverRegister();
|
await mainController.getNewDriverRegister();
|
||||||
Get.to(() => const WelcomeCall());
|
Get.to(() => const WelcomeCall());
|
||||||
@@ -197,24 +194,270 @@ class Main extends StatelessWidget {
|
|||||||
ServiceItem(
|
ServiceItem(
|
||||||
title: "best driver".tr,
|
title: "best driver".tr,
|
||||||
icon: Icons.emoji_events_rounded,
|
icon: Icons.emoji_events_rounded,
|
||||||
|
color: Colors.orangeAccent,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await mainController.getNewDriverRegister();
|
await mainController.getNewDriverRegister();
|
||||||
Get.to(() => DriverTheBest());
|
Get.to(() => DriverTheBest());
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ServiceItem(
|
]),
|
||||||
title: "Add Driver Who Wants to Work".tr,
|
|
||||||
icon: Icons.person_add_alt_1_rounded,
|
const SizedBox(height: 32),
|
||||||
onTap: () {
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHeader() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: AppColor.primaryColor,
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
bottomLeft: Radius.circular(32),
|
||||||
|
bottomRight: Radius.circular(32),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Welcome back,'.tr,
|
||||||
|
style: AppStyle.title.copyWith(color: Colors.white70),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'Service Agent'.tr,
|
||||||
|
style: AppStyle.headTitle2.copyWith(color: Colors.white),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon:
|
||||||
|
const Icon(Icons.refresh_rounded, color: Colors.white),
|
||||||
|
onPressed: () => mainController.refreshDashboardStats(),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 24,
|
||||||
|
backgroundColor: Colors.white24,
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.logout, color: Colors.white),
|
||||||
|
onPressed: () {
|
||||||
|
box.erase();
|
||||||
|
Get.offAllNamed('/login');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
// --- Custom Search Bar ---
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
decoration: AppStyle.boxDecoration.copyWith(
|
||||||
|
color: Colors.white.withValues(alpha: 0.95),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Quick Search...'.tr,
|
||||||
|
border: InputBorder.none,
|
||||||
|
icon: const Icon(Icons.search, color: AppColor.primaryColor),
|
||||||
|
),
|
||||||
|
onSubmitted: (val) {
|
||||||
|
// Global search logic
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatsSection() {
|
||||||
|
return GetBuilder<MainController>(
|
||||||
|
builder: (controller) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 16.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
_buildStatCard(
|
||||||
|
'Pending'.tr,
|
||||||
|
controller.isLoading
|
||||||
|
? '...'
|
||||||
|
: controller.driverWantCompleteRegistration.length
|
||||||
|
.toString(),
|
||||||
|
Icons.timer_rounded,
|
||||||
|
Colors.orange,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
_buildStatCard(
|
||||||
|
'New'.tr,
|
||||||
|
controller.isLoading
|
||||||
|
? '...'
|
||||||
|
: controller.newDriverRegister.length.toString(),
|
||||||
|
Icons.person_add_alt_1_rounded,
|
||||||
|
Colors.green,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
_buildStatCard(
|
||||||
|
'Issues'.tr,
|
||||||
|
controller.isLoading
|
||||||
|
? '...'
|
||||||
|
: controller.driverNotCompleteRegistration.length
|
||||||
|
.toString(),
|
||||||
|
Icons.warning_rounded,
|
||||||
|
Colors.red,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatCard(
|
||||||
|
String title, String count, IconData icon, Color color) {
|
||||||
|
return Expanded(
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: AppStyle.boxDecoration,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Icon(icon, color: color, size: 24),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(count, style: AppStyle.headTitle2.copyWith(fontSize: 20)),
|
||||||
|
Text(title,
|
||||||
|
style: AppStyle.subtitle.copyWith(color: AppColor.accentColor)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCategoryTitle(String title) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 24, 20, 12),
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: AppStyle.headTitle2.copyWith(fontSize: 18),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildGridSection(List<ServiceItem> items) {
|
||||||
|
return GridView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 2,
|
||||||
|
crossAxisSpacing: 12,
|
||||||
|
mainAxisSpacing: 12,
|
||||||
|
childAspectRatio: 1.15,
|
||||||
|
),
|
||||||
|
itemCount: items.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final item = items[index];
|
||||||
|
return _buildServiceCard(item);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildServiceCard(ServiceItem item) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: item.onTap,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
child: Container(
|
||||||
|
decoration: AppStyle.boxDecoration1,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: item.color.withValues(alpha: 0.1),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(item.icon, color: item.color, size: 30),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
|
child: Text(
|
||||||
|
item.title,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: AppStyle.title
|
||||||
|
.copyWith(fontSize: 13, fontWeight: FontWeight.w600),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showSearchDialog({
|
||||||
|
required String title,
|
||||||
|
required TextEditingController controller,
|
||||||
|
required VoidCallback onSearch,
|
||||||
|
TextInputType type = TextInputType.phone,
|
||||||
|
}) {
|
||||||
|
MyDialog().getDialog(
|
||||||
|
title,
|
||||||
|
'Search Details'.tr,
|
||||||
|
Form(
|
||||||
|
key: mainController.formKey,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
MyTextForm(
|
||||||
|
controller: controller,
|
||||||
|
label: title,
|
||||||
|
hint: title,
|
||||||
|
type: type,
|
||||||
|
validator: (val) {
|
||||||
|
if (val == null || val.isEmpty) {
|
||||||
|
return 'Please enter a value'.tr;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onSearch,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showAddDriverDialog() {
|
||||||
Get.defaultDialog(
|
Get.defaultDialog(
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
title: "Add Driver Who Wants to Work".tr,
|
title: "Add Driver Who Wants to Work".tr,
|
||||||
|
titleStyle: AppStyle.headTitle2,
|
||||||
content: SizedBox(
|
content: SizedBox(
|
||||||
width: Get.width * .7,
|
width: Get.width * .8,
|
||||||
height: 300,
|
height: 350,
|
||||||
child: Form(
|
child: Form(
|
||||||
key: mainController.formKey,
|
key: mainController.formKey,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
children: [
|
children: [
|
||||||
MyTextForm(
|
MyTextForm(
|
||||||
controller: mainController.driverNameController,
|
controller: mainController.driverNameController,
|
||||||
@@ -244,17 +487,17 @@ class Main extends StatelessWidget {
|
|||||||
MyTextForm(
|
MyTextForm(
|
||||||
controller: mainController.birthDateController,
|
controller: mainController.birthDateController,
|
||||||
label: 'Insert birth_date of Driver'.tr,
|
label: 'Insert birth_date of Driver'.tr,
|
||||||
hint: 'Insert license type of Driver'.tr,
|
hint: 'YYYY-MM-DD'.tr,
|
||||||
type: TextInputType.number),
|
type: TextInputType.datetime),
|
||||||
],
|
],
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
confirm: MyElevatedButton(
|
confirm: MyElevatedButton(
|
||||||
title: 'Add'.tr,
|
title: 'Add'.tr,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
// if (mainController.formKey.currentState!.validate()) {
|
var res =
|
||||||
var res = await CRUD()
|
await CRUD().post(link: AppLink.addDriverWantWork, payload: {
|
||||||
.post(link: AppLink.addDriverWantWork, payload: {
|
|
||||||
"driver_name": mainController.driverNameController.text,
|
"driver_name": mainController.driverNameController.text,
|
||||||
"national_id": mainController.nationalIdController.text,
|
"national_id": mainController.nationalIdController.text,
|
||||||
"birth_date": mainController.birthDateController.text,
|
"birth_date": mainController.birthDateController.text,
|
||||||
@@ -270,35 +513,31 @@ class Main extends StatelessWidget {
|
|||||||
mainController.licenseTypeController.clear();
|
mainController.licenseTypeController.clear();
|
||||||
mainController.siteDriverController.clear();
|
mainController.siteDriverController.clear();
|
||||||
mainController.phoneController.clear();
|
mainController.phoneController.clear();
|
||||||
Get.snackbar('done', '',
|
Get.snackbar('Success'.tr, 'Added successfully'.tr,
|
||||||
backgroundColor: AppColor.greenColor);
|
backgroundColor: AppColor.greenColor, colorText: Colors.white);
|
||||||
}
|
}
|
||||||
// }
|
|
||||||
},
|
},
|
||||||
kolor: AppColor.greenColor,
|
kolor: AppColor.greenColor,
|
||||||
),
|
),
|
||||||
cancel: MyElevatedButton(
|
cancel: MyElevatedButton(
|
||||||
title: 'Cancel'.tr,
|
title: 'Cancel'.tr,
|
||||||
kolor: AppColor.redColor,
|
kolor: AppColor.redColor,
|
||||||
onPressed: () {
|
onPressed: () => Get.back()),
|
||||||
Get.back();
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
),
|
|
||||||
ServiceItem(
|
void _showAddCarDialog() {
|
||||||
title: "Add Car Who Wants to Work".tr,
|
|
||||||
icon: Icons.directions_car_filled_rounded,
|
|
||||||
onTap: () {
|
|
||||||
Get.defaultDialog(
|
Get.defaultDialog(
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
title: "Add Car Who Wants to Work".tr,
|
title: "Add Car Who Wants to Work".tr,
|
||||||
|
titleStyle: AppStyle.headTitle2,
|
||||||
content: SizedBox(
|
content: SizedBox(
|
||||||
width: Get.width * .7,
|
width: Get.width * .8,
|
||||||
height: 300,
|
height: 350,
|
||||||
child: Form(
|
child: Form(
|
||||||
key: mainController.formKey,
|
key: mainController.formKey,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
children: [
|
children: [
|
||||||
MyTextForm(
|
MyTextForm(
|
||||||
controller: mainController.carOwnerWorkController,
|
controller: mainController.carOwnerWorkController,
|
||||||
@@ -338,159 +577,59 @@ class Main extends StatelessWidget {
|
|||||||
MyTextForm(
|
MyTextForm(
|
||||||
controller: mainController.registrationDateController,
|
controller: mainController.registrationDateController,
|
||||||
label: 'Insert registration_date of Car'.tr,
|
label: 'Insert registration_date of Car'.tr,
|
||||||
hint: 'Insert registration_date of Car'.tr,
|
hint: 'YYYY-MM-DD'.tr,
|
||||||
type: TextInputType.datetime),
|
type: TextInputType.datetime),
|
||||||
],
|
],
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
confirm: MyElevatedButton(
|
confirm: MyElevatedButton(
|
||||||
title: 'Add'.tr,
|
title: 'Add'.tr,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
// if (mainController.formKey.currentState!.validate()) {
|
var res = await CRUD().post(link: AppLink.addCarWantWork, payload: {
|
||||||
var res =
|
|
||||||
await CRUD().post(link: AppLink.addCarWantWork, payload: {
|
|
||||||
"owner_name": mainController.carOwnerWorkController.text,
|
"owner_name": mainController.carOwnerWorkController.text,
|
||||||
"car_number": mainController.carNumberController.text,
|
"car_number": mainController.carNumberController.text,
|
||||||
"manufacture_year":
|
"manufacture_year": mainController.manufactureYearController.text,
|
||||||
mainController.manufactureYearController.text,
|
|
||||||
"car_model": mainController.carModelController.text,
|
"car_model": mainController.carModelController.text,
|
||||||
"car_type": mainController.carTypeController.text,
|
"car_type": mainController.carTypeController.text,
|
||||||
"site": mainController.siteCarController.text,
|
"site": mainController.siteCarController.text,
|
||||||
"registration_date":
|
"registration_date": mainController.registrationDateController.text,
|
||||||
mainController.registrationDateController.text,
|
|
||||||
"phone": mainController.phoneCarController.text,
|
"phone": mainController.phoneCarController.text,
|
||||||
});
|
});
|
||||||
Log.print('res: ${res}');
|
|
||||||
if (res != 'failure' && res['status'] == 'success') {
|
if (res != 'failure' && res['status'] == 'success') {
|
||||||
Get.back();
|
Get.back();
|
||||||
mainController.ownerController.clear();
|
mainController.carOwnerWorkController.clear();
|
||||||
mainController.carNumberController.clear();
|
mainController.carNumberController.clear();
|
||||||
mainController.manufactureYearController.clear();
|
mainController.manufactureYearController.clear();
|
||||||
mainController.carModelController.clear();
|
mainController.carModelController.clear();
|
||||||
mainController.siteCarController.clear();
|
mainController.siteCarController.clear();
|
||||||
mainController.carTypeController.clear();
|
mainController.carTypeController.clear();
|
||||||
mainController.registrationDateController.clear();
|
mainController.registrationDateController.clear();
|
||||||
mainController.phoneController.clear();
|
mainController.phoneCarController.clear();
|
||||||
Get.snackbar('done', '',
|
Get.snackbar('Success'.tr, 'Added successfully'.tr,
|
||||||
backgroundColor: AppColor.greenColor);
|
backgroundColor: AppColor.greenColor, colorText: Colors.white);
|
||||||
}
|
}
|
||||||
// }
|
|
||||||
},
|
},
|
||||||
kolor: AppColor.greenColor,
|
kolor: AppColor.greenColor,
|
||||||
),
|
),
|
||||||
cancel: MyElevatedButton(
|
cancel: MyElevatedButton(
|
||||||
title: 'Cancel'.tr,
|
title: 'Cancel'.tr,
|
||||||
kolor: AppColor.redColor,
|
kolor: AppColor.redColor,
|
||||||
onPressed: () {
|
onPressed: () => Get.back()),
|
||||||
Get.back();
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
// --- Building the UI ---
|
|
||||||
return MyScaffold(
|
|
||||||
title: 'Intaleq Service'.tr,
|
|
||||||
isleading: false,
|
|
||||||
action: MyElevatedButton(
|
|
||||||
title: 'Logout'.tr,
|
|
||||||
onPressed: () async {
|
|
||||||
// box.write(BoxName.employeename, 'masa');
|
|
||||||
print(box.read(BoxName.employeename).toString());
|
|
||||||
},
|
|
||||||
kolor: AppColor.redColor,
|
|
||||||
),
|
|
||||||
// The body now uses a GridView for a better layout.
|
|
||||||
// You can replace the color with your main theme color.
|
|
||||||
// backgroundColor: const Color(0xFFF5F7FA),
|
|
||||||
body: [
|
|
||||||
GridView.builder(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
itemCount: services.length,
|
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
|
||||||
crossAxisCount: 2, // Two columns
|
|
||||||
crossAxisSpacing: 16.0, // Horizontal space between cards
|
|
||||||
mainAxisSpacing: 16.0, // Vertical space between cards
|
|
||||||
childAspectRatio: 1.1, // Adjust card shape (width/height ratio)
|
|
||||||
),
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final service = services[index];
|
|
||||||
return ServiceCard(
|
|
||||||
title: service.title,
|
|
||||||
icon: service.icon,
|
|
||||||
onTap: service.onTap,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Service Card Widget ---
|
class ServiceItem {
|
||||||
// A reusable widget for displaying each service.
|
|
||||||
class ServiceCard extends StatelessWidget {
|
|
||||||
final String title;
|
final String title;
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
|
final Color color;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
|
|
||||||
const ServiceCard({
|
ServiceItem({
|
||||||
super.key,
|
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.icon,
|
required this.icon,
|
||||||
|
required this.color,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return InkWell(
|
|
||||||
onTap: onTap,
|
|
||||||
borderRadius: BorderRadius.circular(16.0),
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(16.0),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.grey.withOpacity(0.15),
|
|
||||||
spreadRadius: 2,
|
|
||||||
blurRadius: 8,
|
|
||||||
offset: const Offset(0, 4), // changes position of shadow
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
// Icon
|
|
||||||
Icon(
|
|
||||||
icon,
|
|
||||||
size: 48.0,
|
|
||||||
// You can replace this color with your AppStyle color
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
// Title
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
|
||||||
child: Text(
|
|
||||||
title,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontSize: 14,
|
|
||||||
// You can replace this color with your AppStyle color
|
|
||||||
color: Color(0xFF4A4A4A),
|
|
||||||
),
|
|
||||||
maxLines: 2,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ class MyElevatedButton extends StatelessWidget {
|
|||||||
final VoidCallback onPressed;
|
final VoidCallback onPressed;
|
||||||
final Color kolor;
|
final Color kolor;
|
||||||
final int vibrateDuration;
|
final int vibrateDuration;
|
||||||
|
final bool loading;
|
||||||
|
final Widget? child;
|
||||||
|
|
||||||
const MyElevatedButton({
|
const MyElevatedButton({
|
||||||
Key? key,
|
Key? key,
|
||||||
@@ -20,6 +22,8 @@ class MyElevatedButton extends StatelessWidget {
|
|||||||
required this.onPressed,
|
required this.onPressed,
|
||||||
this.kolor = AppColor.primaryColor,
|
this.kolor = AppColor.primaryColor,
|
||||||
this.vibrateDuration = 100,
|
this.vibrateDuration = 100,
|
||||||
|
this.loading = false,
|
||||||
|
this.child,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -33,7 +37,9 @@ class MyElevatedButton extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(12.0),
|
borderRadius: BorderRadius.circular(12.0),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: loading
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
if (vibrate == true) {
|
if (vibrate == true) {
|
||||||
if (Platform.isIOS) {
|
if (Platform.isIOS) {
|
||||||
HapticFeedback.selectionClick();
|
HapticFeedback.selectionClick();
|
||||||
@@ -43,7 +49,17 @@ class MyElevatedButton extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
onPressed();
|
onPressed();
|
||||||
},
|
},
|
||||||
child: Text(
|
child: loading
|
||||||
|
? const SizedBox(
|
||||||
|
height: 20,
|
||||||
|
width: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: Colors.white,
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: child ??
|
||||||
|
Text(
|
||||||
title,
|
title,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: AppStyle.title.copyWith(color: AppColor.secondaryColor),
|
style: AppStyle.title.copyWith(color: AppColor.secondaryColor),
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
import '../../constant/box_name.dart';
|
|
||||||
import '../../constant/colors.dart';
|
import '../../constant/colors.dart';
|
||||||
import '../../constant/style.dart';
|
import '../../constant/style.dart';
|
||||||
import '../../main.dart';
|
|
||||||
|
|
||||||
class MyTextForm extends StatelessWidget {
|
class MyTextForm extends StatelessWidget {
|
||||||
const MyTextForm({
|
const MyTextForm({
|
||||||
@@ -13,10 +11,12 @@ class MyTextForm extends StatelessWidget {
|
|||||||
required this.label,
|
required this.label,
|
||||||
required this.hint,
|
required this.hint,
|
||||||
required this.type,
|
required this.type,
|
||||||
|
this.validator,
|
||||||
});
|
});
|
||||||
final TextEditingController controller;
|
final TextEditingController controller;
|
||||||
final String label, hint;
|
final String label, hint;
|
||||||
final TextInputType type;
|
final TextInputType type;
|
||||||
|
final String? Function(String?)? validator;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -45,8 +45,8 @@ class MyTextForm extends StatelessWidget {
|
|||||||
hintStyle: AppStyle.title,
|
hintStyle: AppStyle.title,
|
||||||
labelStyle: AppStyle.title,
|
labelStyle: AppStyle.title,
|
||||||
),
|
),
|
||||||
validator: (value) {
|
validator: validator ?? (value) {
|
||||||
if (value!.isEmpty) {
|
if (value == null || value.isEmpty) {
|
||||||
return '${'Please enter'.tr} $label.'.tr;
|
return '${'Please enter'.tr} $label.'.tr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +56,6 @@ class MyTextForm extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
} else if (type == TextInputType.phone) {
|
} else if (type == TextInputType.phone) {
|
||||||
if (value.length > 14) {
|
if (value.length > 14) {
|
||||||
//for this you will return to 10 but now for service egypt
|
|
||||||
return 'Please enter a valid phone number.'.tr;
|
return 'Please enter a valid phone number.'.tr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user