Update: 2026-06-26 17:29:23
This commit is contained in:
@@ -1,63 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin/Staff/add_super_admin.php
|
||||
* إضافة مشرف عام (Super Admin) — استخدام لمرة واحدة
|
||||
*/
|
||||
require_once __DIR__ . '/../../core/bootstrap.php';
|
||||
|
||||
// $adminKey = filterRequest('admin_key') ?? '';
|
||||
// $expected = getenv('MIGRATION_ADMIN_KEY');
|
||||
// if (empty($adminKey) || empty($expected) || !hash_equals($expected, $adminKey)) {
|
||||
// http_response_code(403);
|
||||
// exit(json_encode(['error' => 'Access denied. Admin key required.']));
|
||||
// }
|
||||
|
||||
$con = Database::get('main');
|
||||
|
||||
$name = $_GET['name'] ?? filterRequest('name') ?: 'Super Admin';
|
||||
$email = $_GET['email'] ?? filterRequest('email') ?: '';
|
||||
$phone = $_GET['phone'] ?? filterRequest('phone') ?: '';
|
||||
$fingerprint = $_GET['fingerprint'] ?? filterRequest('fingerprint') ?: '';
|
||||
$password = $_GET['password'] ?? filterRequest('password') ?: bin2hex(random_bytes(8));
|
||||
|
||||
try {
|
||||
$hashedPass = password_hash($password, PASSWORD_DEFAULT);
|
||||
$encName = $encryptionHelper->encryptData($name);
|
||||
$encPhone = $phone ? $encryptionHelper->encryptData($phone) : '';
|
||||
$encEmail = $email ? $encryptionHelper->encryptData($email) : '';
|
||||
$encFp = $fingerprint ? $encryptionHelper->encryptData($fingerprint) : '';
|
||||
$fpHash = $fingerprint ? hash('sha256', $fingerprint) : '';
|
||||
$uniqueId = bin2hex(random_bytes(16));
|
||||
|
||||
$check = $con->prepare("SELECT id FROM adminUser WHERE role = 'super_admin' LIMIT 1");
|
||||
$check->execute();
|
||||
if ($check->fetch()) {
|
||||
echo "<h2>⚠️ Super Admin already exists.</h2>";
|
||||
exit;
|
||||
}
|
||||
|
||||
$sql = "INSERT INTO adminUser (id, fingerprint, fingerprint_hash, name, phone, email, password, role, created_at)
|
||||
VALUES (:id, :fp, :fp_hash, :name, :phone, :email, :pass, 'super_admin', NOW())";
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->execute([
|
||||
':id' => $uniqueId,
|
||||
':fp' => $encFp,
|
||||
':fp_hash' => $fpHash,
|
||||
':name' => $encName,
|
||||
':phone' => $encPhone,
|
||||
':email' => $encEmail,
|
||||
':pass' => $hashedPass,
|
||||
]);
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
echo "<h2>✅ Super Admin created successfully!</h2>";
|
||||
echo "<p><b>ID:</b> $uniqueId</p>";
|
||||
echo "<p><b>Name:</b> $name</p>";
|
||||
echo "<p><b>Password:</b> $password</p>";
|
||||
echo "<p style='color:red;'><b>⚠️ Save this password. Delete this file after use.</b></p>";
|
||||
} else {
|
||||
echo "<h2>❌ Failed to create Super Admin.</h2>";
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
echo "<h2>❌ Error: " . htmlspecialchars($e->getMessage()) . "</h2>";
|
||||
}
|
||||
@@ -7,7 +7,6 @@ error_reporting(E_ALL);
|
||||
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
$driverID = filterRequest("driverID");
|
||||
$invoiceNumber = filterRequest("invoiceNumber");
|
||||
$amount = filterRequest("amount");
|
||||
$date = filterRequest("date");
|
||||
@@ -17,7 +16,7 @@ $linkImage = null;
|
||||
$uploadDate = date("Y-m-d H:i:s");
|
||||
|
||||
// ✅ طباعة بيانات الإدخال للتأكد
|
||||
error_log("[add_invoice.php] 📥 Data received | driverID: $driverID, invoiceNumber: $invoiceNumber, amount: $amount, date: $date");
|
||||
error_log("[add_invoice.php] 📥 Data received | invoiceNumber: $invoiceNumber, amount: $amount, date: $date");
|
||||
|
||||
// التحقق من وجود ملف الصورة
|
||||
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
|
||||
@@ -43,7 +42,7 @@ if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$new_filename = $invoiceNumber . "_" . $driverID . '.' . $image_extension;
|
||||
$new_filename = $invoiceNumber . '.' . $image_extension;
|
||||
$target_dir = "invoice_images/";
|
||||
$target_file = $target_dir . $new_filename;
|
||||
|
||||
@@ -66,9 +65,9 @@ if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $con->prepare("INSERT INTO invoice_records (driverID, invoice_number,name, amount, date, image_link, created_at)
|
||||
VALUES (?, ?, ?,?, ?, ?, ?)");
|
||||
$stmt->execute([$driverID, $invoiceNumber,$name, $amount, $date, $linkImage, $uploadDate]);
|
||||
$stmt = $con->prepare("INSERT INTO invoice_records (invoice_number, name, amount, date, image_link, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$invoiceNumber, $name, $amount, $date, $linkImage, $uploadDate]);
|
||||
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../core/bootstrap.php';
|
||||
|
||||
try {
|
||||
$con = Database::get('main');
|
||||
|
||||
// Check if columns already exist to avoid errors
|
||||
$check = $con->query("SHOW COLUMNS FROM adminUser LIKE 'status'");
|
||||
if ($check->rowCount() == 0) {
|
||||
$sql = "ALTER TABLE adminUser
|
||||
ADD COLUMN status ENUM('pending', 'approved', 'suspended', 'rejected') NOT NULL DEFAULT 'pending' AFTER role,
|
||||
ADD COLUMN phone VARCHAR(50) DEFAULT NULL AFTER name,
|
||||
ADD COLUMN email VARCHAR(255) DEFAULT NULL AFTER phone,
|
||||
ADD COLUMN approved_by VARCHAR(64) DEFAULT NULL AFTER status,
|
||||
ADD COLUMN approved_at DATETIME DEFAULT NULL AFTER approved_by";
|
||||
|
||||
$con->exec($sql);
|
||||
|
||||
// Update existing admins to approved and super_admin
|
||||
$con->exec("UPDATE adminUser SET status = 'approved', role = 'super_admin' WHERE id IS NOT NULL");
|
||||
|
||||
echo json_encode(["status" => "success", "message" => "Migration completed successfully."]);
|
||||
} else {
|
||||
echo json_encode(["status" => "success", "message" => "Columns already exist."]);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(["status" => "error", "message" => "An internal error occurred"]);
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// Admin/auth/migration_cryptography.php
|
||||
// سكريبت لترحيل التشفير القديم (CBC) إلى التشفير الجديد (AES-256-GCM)
|
||||
// يمكن تشغيله عبر الـ CLI أو المتصفح (بصلاحيات مسؤول).
|
||||
// ============================================================
|
||||
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
echo "Starting Cryptography Migration to AES-256-GCM...\n";
|
||||
ob_flush(); flush();
|
||||
|
||||
$tables = [
|
||||
'driver' => [
|
||||
'phone', 'email', 'gender', 'birthdate', 'site',
|
||||
'first_name', 'last_name', 'accountBank', 'education',
|
||||
'employmentType', 'maritalStatus', 'national_number',
|
||||
'name_arabic', 'address'
|
||||
],
|
||||
'passengers' => [
|
||||
'phone', 'email', 'gender', 'birthdate',
|
||||
'first_name', 'last_name', 'token'
|
||||
],
|
||||
'CarRegistration' => [
|
||||
'vin', 'car_plate', 'owner', 'address'
|
||||
],
|
||||
'carPlateEdit' => [
|
||||
'carPlate', 'owner'
|
||||
],
|
||||
'phone_verification' => [
|
||||
'phone_number'
|
||||
],
|
||||
'phone_verification_passenger' => [
|
||||
'phone_number'
|
||||
],
|
||||
'driverToken' => [
|
||||
'token'
|
||||
],
|
||||
'passengerToken' => [
|
||||
'token'
|
||||
],
|
||||
'mishwari' => [
|
||||
'phone', 'gender', 'name', 'name_english', 'car_plate', 'token', 'education', 'national_number', 'age'
|
||||
],
|
||||
'rate_app' => [
|
||||
'email', 'phone'
|
||||
],
|
||||
'admins' => [
|
||||
'name', 'phone', 'email', 'fp'
|
||||
],
|
||||
'driver_assurance' => [
|
||||
'assured', 'health_insurance_provider'
|
||||
],
|
||||
'blacklist_drivers' => [
|
||||
'phone'
|
||||
],
|
||||
'blacklist_passengers' => [
|
||||
'phone'
|
||||
],
|
||||
'feedBack' => [
|
||||
'feedBack'
|
||||
]
|
||||
];
|
||||
|
||||
$totalUpdated = 0;
|
||||
|
||||
foreach ($tables as $table => $columns) {
|
||||
echo "Processing table: $table ...\n";
|
||||
ob_flush(); flush();
|
||||
|
||||
try {
|
||||
$sql = "SELECT `id`, `" . implode("`, `", $columns) . "` FROM `$table`";
|
||||
$stmt = $con->query($sql);
|
||||
if (!$stmt) {
|
||||
echo "Skipped $table (Not found or missing columns).\n";
|
||||
continue;
|
||||
}
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (Exception $e) {
|
||||
echo "An internal error occurred" . "\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
$tableUpdatedCount = 0;
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$id = $row['id'];
|
||||
$needsUpdate = false;
|
||||
$updateValues = [];
|
||||
$params = [':id' => $id];
|
||||
|
||||
foreach ($columns as $col) {
|
||||
$value = $row[$col];
|
||||
|
||||
// تحقق إذا كان الحقل يحتوي على قيمة وإذا لم يكن مشفر بالنظام الجديد
|
||||
if (!empty($value) && strpos($value, 'GCM:') !== 0) {
|
||||
// محاولة فك التشفير القديم (CBC)
|
||||
try {
|
||||
$decrypted = $encryptionHelper->decryptData($value);
|
||||
if ($decrypted !== false && $decrypted !== '') {
|
||||
// إعادة التشفير (سيستخدم GCM الآن)
|
||||
$newEncrypted = $encryptionHelper->encryptData($decrypted);
|
||||
$updateValues[] = "`$col` = :$col";
|
||||
$params[":$col"] = $newEncrypted;
|
||||
$needsUpdate = true;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
error_log("Failed to migrate $col for ID $id in $table: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($needsUpdate) {
|
||||
$setClause = implode(", ", $updateValues);
|
||||
$updateSql = "UPDATE `$table` SET $setClause WHERE `id` = :id";
|
||||
$updateStmt = $con->prepare($updateSql);
|
||||
$updateStmt->execute($params);
|
||||
$tableUpdatedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
echo "Finished $table. Updated rows: $tableUpdatedCount\n";
|
||||
$totalUpdated += $tableUpdatedCount;
|
||||
ob_flush(); flush();
|
||||
}
|
||||
|
||||
echo "Migration completed! Total rows updated: $totalUpdated\n";
|
||||
?>
|
||||
@@ -30,7 +30,7 @@ class SiroGeminiService {
|
||||
float $siroBasePrice,
|
||||
string $regionName,
|
||||
string $countryCode,
|
||||
string $model = 'gemini-1.5-flash'
|
||||
string $model = 'gemini-flash-lite-latest'
|
||||
): ?array {
|
||||
if (!$this->apiKey) {
|
||||
error_log("[SiroGeminiService] API Key is missing.");
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
<?php
|
||||
header('Content-Type: text/plain; charset=UTF-8');
|
||||
require_once __DIR__ . '/core/bootstrap.php';
|
||||
|
||||
echo "--- 🔍 SIRO FINGERPRINT DIAGNOSTIC TOOL ---\n\n";
|
||||
|
||||
try {
|
||||
$con = Database::get('main');
|
||||
echo "✅ Database connection successful.\n";
|
||||
} catch (Exception $e) {
|
||||
echo "❌ Database connection failed: " . $e->getMessage() . "\n";
|
||||
exit;
|
||||
}
|
||||
|
||||
$targetId = 'e494c5750f95e1c26654';
|
||||
echo "Target Passenger ID: " . $targetId . "\n\n";
|
||||
|
||||
try {
|
||||
$stmt = $con->prepare("SELECT * FROM tokens WHERE passengerID = ? LIMIT 1");
|
||||
$stmt->execute([$targetId]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($row) {
|
||||
echo "✅ Token row found:\n";
|
||||
echo " - Passenger ID: " . $row['passengerID'] . "\n";
|
||||
echo " - Token (encrypted/raw): " . $row['token'] . "\n";
|
||||
echo " - Fingerprint (stored): " . $row['fingerprint'] . "\n";
|
||||
|
||||
$decryptedToken = $encryptionHelper->decryptData($row['token']);
|
||||
echo " - Decrypted Token: " . $decryptedToken . "\n";
|
||||
|
||||
$fpPepper = getenv('FP_PEPPER') ?: '';
|
||||
echo " - FP_PEPPER: " . ($fpPepper ? "Set" : "Not Set") . "\n";
|
||||
} else {
|
||||
echo "❌ No token row found for passenger!\n";
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
echo "❌ Error: " . $e->getMessage() . "\n";
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
<?php
|
||||
header('Content-Type: text/plain; charset=UTF-8');
|
||||
require_once __DIR__ . '/core/bootstrap.php';
|
||||
|
||||
echo "--- 🔍 SIRO LOGIN DIAGNOSTIC TOOL ---\n\n";
|
||||
|
||||
try {
|
||||
$con = Database::get('main');
|
||||
echo "✅ Database connection successful.\n";
|
||||
} catch (Exception $e) {
|
||||
echo "❌ Database connection failed: " . $e->getMessage() . "\n";
|
||||
exit;
|
||||
}
|
||||
|
||||
$targetId = 'e494c5750f95e1c26654';
|
||||
echo "Target Passenger ID: " . $targetId . "\n\n";
|
||||
|
||||
// 1. Check passengers table
|
||||
try {
|
||||
$stmt = $con->prepare("SELECT * FROM passengers WHERE id = ?");
|
||||
$stmt->execute([$targetId]);
|
||||
$p = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($p) {
|
||||
echo "✅ Passenger found in database:\n";
|
||||
echo " - Phone (encrypted): " . $p['phone'] . "\n";
|
||||
echo " - Phone (decrypted): " . $encryptionHelper->decryptData($p['phone']) . "\n";
|
||||
echo " - First Name (decrypted): " . $encryptionHelper->decryptData($p['first_name']) . "\n";
|
||||
echo " - Last Name (decrypted): " . $encryptionHelper->decryptData($p['last_name']) . "\n";
|
||||
} else {
|
||||
echo "❌ Passenger NOT found in database!\n";
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
echo "❌ Error querying passengers table: " . $e->getMessage() . "\n";
|
||||
}
|
||||
|
||||
// 2. Check phone_verification_passenger table
|
||||
try {
|
||||
$decryptedPhone = '962798583052';
|
||||
$encryptedPhone = $encryptionHelper->encryptData($decryptedPhone);
|
||||
echo "\nSearching phone_verification_passenger for: $decryptedPhone ($encryptedPhone)\n";
|
||||
|
||||
$stmt = $con->prepare("SELECT * FROM phone_verification_passenger WHERE phone_number = ?");
|
||||
$stmt->execute([$encryptedPhone]);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
if (!empty($rows)) {
|
||||
echo "✅ Phone verification rows found (" . count($rows) . "):\n";
|
||||
foreach ($rows as $row) {
|
||||
echo " - ID: " . $row['id'] . " | Verified: " . $row['verified'] . " | Expiration: " . $row['expiration_time'] . " | Created At: " . $row['created_at'] . "\n";
|
||||
}
|
||||
} else {
|
||||
echo "❌ No phone verification rows found for this phone number!\n";
|
||||
|
||||
// Let's get any recent rows
|
||||
$stmt2 = $con->prepare("SELECT * FROM phone_verification_passenger ORDER BY id DESC LIMIT 5");
|
||||
$stmt2->execute();
|
||||
$all = $stmt2->fetchAll(PDO::FETCH_ASSOC);
|
||||
echo " Recent rows in table:\n";
|
||||
foreach ($all as $row) {
|
||||
$dec = $encryptionHelper->decryptData($row['phone_number']);
|
||||
echo " - ID: " . $row['id'] . " | Phone (decrypted): $dec | Verified: " . $row['verified'] . "\n";
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
echo "❌ Error querying phone_verification_passenger: " . $e->getMessage() . "\n";
|
||||
}
|
||||
|
||||
// 3. Test the exact SQL query from loginFromGooglePassenger.php
|
||||
echo "\n--- 🧪 Testing loginFromGooglePassenger query (WITH verified = 1 constraint) ---\n";
|
||||
try {
|
||||
$sqlOld = "SELECT p.`id` FROM passengers p
|
||||
LEFT JOIN phone_verification_passenger ON phone_verification_passenger.phone_number = p.phone
|
||||
WHERE p.id = :id AND phone_verification_passenger.verified = '1'";
|
||||
|
||||
$stmt = $con->prepare($sqlOld);
|
||||
$stmt->execute([':id' => $targetId]);
|
||||
$count = $stmt->rowCount();
|
||||
echo "Old query row count: $count\n";
|
||||
} catch (Exception $e) {
|
||||
echo "Old query error: " . $e->getMessage() . "\n";
|
||||
}
|
||||
|
||||
echo "\n--- 🧪 Testing loginFromGooglePassenger query (WITHOUT verified = 1 constraint) ---\n";
|
||||
try {
|
||||
$sqlNew = "SELECT p.`id` FROM passengers p
|
||||
LEFT JOIN phone_verification_passenger ON phone_verification_passenger.phone_number = p.phone
|
||||
WHERE p.id = :id";
|
||||
|
||||
$stmt = $con->prepare($sqlNew);
|
||||
$stmt->execute([':id' => $targetId]);
|
||||
$count = $stmt->rowCount();
|
||||
echo "New query row count: $count\n";
|
||||
} catch (Exception $e) {
|
||||
echo "New query error: " . $e->getMessage() . "\n";
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
<?php
|
||||
// migrate_driver_passwords.php
|
||||
// سكربت لمرة واحدة لإعادة توليد كلمة السر لكل السائقين
|
||||
// المعادلة: baseString = id | normalizedPhone | [national_number OR birthYear]
|
||||
// السر: hmacHex = hash_hmac('sha256', baseString, SECRET_KEY_HMAC, false)
|
||||
// المخزن في DB: password_hash(hmacHex)
|
||||
|
||||
set_time_limit(0); // منع انتهاء المهلة أثناء المايغريشن
|
||||
ini_set('memory_limit', '512M');
|
||||
|
||||
require_once realpath(__DIR__ . '/../vendor/autoload.php');
|
||||
require_once 'load_env.php';
|
||||
|
||||
$env_file = getenv('ENV_FILE_PATH') ?: (__DIR__ . '/../../../env/.env');
|
||||
loadEnvironment($env_file);
|
||||
|
||||
include "encrypt_decrypt.php"; // لاستخدام $encryptionHelper
|
||||
|
||||
$dbUser = getenv('USER');
|
||||
$dbPass = getenv('PASS');
|
||||
$dbname = getenv('dbname');
|
||||
$pepper = getenv('SECRET_KEY_HMAC');
|
||||
|
||||
if ($dbUser === false || $dbPass === false || $dbname === false || $pepper === false) {
|
||||
error_log("[MIGRATE] Missing env vars (USER/PASS/dbname/SECRET_KEY_HMAC). Abort.");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
$dsn = "mysql:host=localhost;dbname={$dbname};charset=utf8mb4";
|
||||
$options = [
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES UTF8",
|
||||
];
|
||||
$pdo = new PDO($dsn, $dbUser, $dbPass, $options);
|
||||
|
||||
// نجلب الحقول التي نحتاجها لبناء السر
|
||||
$sql = "SELECT id, phone, birthdate, national_number FROM driver";
|
||||
$stmt = $pdo->query($sql);
|
||||
|
||||
$update = $pdo->prepare("UPDATE driver SET password = :pwd WHERE id = :id");
|
||||
|
||||
$count = 0;
|
||||
$skipped = 0;
|
||||
$startTime = microtime(true);
|
||||
|
||||
while ($row = $stmt->fetch()) {
|
||||
$id = $row['id'];
|
||||
$encPhone = $row['phone'];
|
||||
$encBirth = $row['birthdate'] ?? null;
|
||||
$encNat = $row['national_number'] ?? null;
|
||||
|
||||
// نفك التشفير – قد يرجع null لو الحقل فاضي
|
||||
$phone = $encPhone ? $encryptionHelper->decryptData($encPhone) : null;
|
||||
$birth = $encBirth ? $encryptionHelper->decryptData($encBirth) : null;
|
||||
$nat = $encNat ? $encryptionHelper->decryptData($encNat) : null;
|
||||
|
||||
if (empty($id) || empty($phone)) {
|
||||
// لو ناقصين، نتجاوز السطر مع تسجيل في اللوج
|
||||
error_log("[MIGRATE] Skip driver id={$id}: missing phone or id.");
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// في الوضع المثالي عندك nat + birthdate لكل السائقين
|
||||
// لو حاب تجبرهم يكونوا موجودين:
|
||||
/*
|
||||
if (empty($nat) || empty($birth)) {
|
||||
error_log("[MIGRATE] Skip driver id={$id}: missing nat or birthdate.");
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
*/
|
||||
|
||||
// phone مفروض يكون أصلاً مطبّع (9639...) من سكربت التسجيل
|
||||
$normalizedPhone = trim($phone);
|
||||
|
||||
// نبني baseString: الأساس id + phone
|
||||
$parts = [$id, $normalizedPhone];
|
||||
|
||||
// نضيف رقم وطني أو سنة الميلاد (حسب الموجود)
|
||||
if (!empty($nat)) {
|
||||
$parts[] = trim($nat);
|
||||
} elseif (!empty($birth)) {
|
||||
// birthdate متوقعة بصيغة YYYY-01-01 -> نأخذ السنة فقط
|
||||
$year = substr($birth, 0, 4);
|
||||
if (preg_match('/^\d{4}$/', $year)) {
|
||||
$parts[] = $year;
|
||||
}
|
||||
}
|
||||
|
||||
$baseString = implode('|', $parts);
|
||||
|
||||
// اشتقاق السر النهائي (HEX string، بدون باينري)
|
||||
$hmacHex = hash_hmac('sha256', $baseString, $pepper, false);
|
||||
|
||||
// نخزن فقط الهاش باستخدام password_hash
|
||||
$pwdHash = password_hash($hmacHex, PASSWORD_DEFAULT);
|
||||
|
||||
$update->execute([
|
||||
':pwd' => $pwdHash,
|
||||
':id' => $id,
|
||||
]);
|
||||
|
||||
$count++;
|
||||
|
||||
// لوج بسيط كل 100 سائق
|
||||
if ($count % 100 === 0) {
|
||||
$elapsed = round(microtime(true) - $startTime, 2);
|
||||
error_log("[MIGRATE] Progress: updated {$count} drivers, skipped {$skipped}, elapsed {$elapsed}s");
|
||||
}
|
||||
}
|
||||
|
||||
$totalTime = round(microtime(true) - $startTime, 2);
|
||||
error_log("[MIGRATE] Done. Updated {$count} driver passwords, skipped {$skipped}. Total time: {$totalTime}s");
|
||||
echo "Migration finished. Updated {$count} drivers, skipped {$skipped}. Time: {$totalTime}s\n";
|
||||
|
||||
} catch (PDOException $e) {
|
||||
error_log("[MIGRATE][PDO] " . $e->getMessage());
|
||||
echo "Migration failed (DB error).\n";
|
||||
exit(1);
|
||||
} catch (Exception $e) {
|
||||
error_log("[MIGRATE][GENERAL] " . $e->getMessage());
|
||||
echo "Migration failed (general error).\n";
|
||||
exit(1);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/core/bootstrap.php';
|
||||
|
||||
try {
|
||||
$con = Database::get('main');
|
||||
$sql = "CREATE TABLE IF NOT EXISTS `passenger_opening_locations` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`passenger_id` varchar(100) NOT NULL,
|
||||
`latitude` varchar(30) NOT NULL,
|
||||
`longitude` varchar(30) NOT NULL,
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_passenger_id` (`passenger_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;";
|
||||
|
||||
$con->exec($sql);
|
||||
echo "SUCCESS: passenger_opening_locations table created successfully.\n";
|
||||
} catch (Exception $e) {
|
||||
echo "An internal error occurred" . "\n";
|
||||
}
|
||||
?>
|
||||
@@ -1,210 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* test_add_driver_and_car.php
|
||||
* ===========================
|
||||
* يضيف سائق + سيارته في قاعدة البيانات مباشرة (لأغراض الاختبار).
|
||||
* يستخدم نفس التشفير ونظام إدارة الهوية مثل الإنتاج.
|
||||
*
|
||||
* الاستخدام:
|
||||
* https://example.com/backend/test_add_driver_and_car.php?phone=96279xxxxxxx&password=1234&first_name=Ahmed&last_name=Ali&make=Hyundai&model=Elantra&year=2020&car_plate=1234&color=White
|
||||
*
|
||||
* جميع الحقول الاختيارية لها قيمة افتراضية.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/core/bootstrap.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
// دمج GET + POST + JSON body
|
||||
$rawBody = file_get_contents('php://input');
|
||||
$json = $rawBody ? json_decode($rawBody, true) : [];
|
||||
$_POST = array_merge($_GET, $_POST, $json ?: []);
|
||||
|
||||
try {
|
||||
/* ================== قراءة المدخلات ================== */
|
||||
$phone = filterRequest('phone');
|
||||
$password = filterRequest('password');
|
||||
$first_name = filterRequest('first_name');
|
||||
$last_name = filterRequest('last_name');
|
||||
|
||||
if (empty($phone) || empty($password) || empty($first_name) || empty($last_name)) {
|
||||
jsonError('Required: phone, password, first_name, last_name');
|
||||
exit;
|
||||
}
|
||||
|
||||
// توحيد الرقم (إزالة +/مسافات)
|
||||
$phone = preg_replace('/[ \-\(\)\+]/', '', $phone);
|
||||
|
||||
// حقول السائق الاختيارية
|
||||
$email = filterRequest('email') ?: $phone . '@intaleqapp.com';
|
||||
$gender = filterRequest('gender') ?: 'Male';
|
||||
$national_number = filterRequest('national_number') ?: '';
|
||||
$birthdate = filterRequest('birthdate') ?: '1990-01-01';
|
||||
$site = filterRequest('site') ?: 'testing';
|
||||
$license_type = filterRequest('license_type') ?: 'private';
|
||||
$employmentType = filterRequest('employmentType') ?: 'full_time';
|
||||
|
||||
// حقول السيارة
|
||||
$make = filterRequest('make') ?: 'Toyota';
|
||||
$model = filterRequest('model') ?: 'Camry';
|
||||
$year = filterRequest('year') ?: '2020';
|
||||
$car_plate = filterRequest('car_plate') ?: 'TEST' . random_int(100, 999);
|
||||
$vin = filterRequest('vin') ?: 'VIN' . bin2hex(random_bytes(8));
|
||||
$color = filterRequest('color') ?: 'White';
|
||||
$color_hex = filterRequest('color_hex') ?: '#FFFFFF';
|
||||
$fuel = filterRequest('fuel') ?: 'Petrol';
|
||||
$owner = filterRequest('owner') ?: trim($first_name . ' ' . $last_name);
|
||||
$expiration_date = filterRequest('expiration_date') ?: date('Y-m-d', strtotime('+1 year'));
|
||||
|
||||
/* ================== ID السائق ================== */
|
||||
$driverId = 'TEST' . date('YmdHis') . random_int(1000, 9999);
|
||||
|
||||
/* ================== التشفير ================== */
|
||||
$encPhone = $encryptionHelper->encryptData($phone);
|
||||
$encEmail = $encryptionHelper->encryptData($email);
|
||||
$encFirstName = $encryptionHelper->encryptData($first_name);
|
||||
$encLastName = $encryptionHelper->encryptData($last_name);
|
||||
$encNameArabic = $encryptionHelper->encryptData("$first_name $last_name");
|
||||
$encGender = $encryptionHelper->encryptData($gender);
|
||||
$encNationalNumber = $national_number ? $encryptionHelper->encryptData($national_number) : '';
|
||||
$encBirthdate = $encryptionHelper->encryptData($birthdate);
|
||||
$encSite = $encryptionHelper->encryptData($site);
|
||||
$encOwner = $encryptionHelper->encryptData($owner);
|
||||
$encCarPlate = $encryptionHelper->encryptData($car_plate);
|
||||
$encVin = $encryptionHelper->encryptData($vin);
|
||||
|
||||
$passwordHashed = password_hash($password, PASSWORD_DEFAULT);
|
||||
|
||||
$con = Database::get('main');
|
||||
|
||||
/* ================== التحقق من التكرار ================== */
|
||||
$dup = $con->prepare("SELECT id FROM driver WHERE phone = :p OR email = :e");
|
||||
$dup->execute([':p' => $encPhone, ':e' => $encEmail]);
|
||||
if ($dup->rowCount() > 0) {
|
||||
jsonError("Phone or email already registered.");
|
||||
exit;
|
||||
}
|
||||
|
||||
$con->beginTransaction();
|
||||
|
||||
/* ================== 1) إدراج السائق ================== */
|
||||
$sqlDriver = "
|
||||
INSERT INTO driver (
|
||||
id, phone, email, password, gender, license_type, national_number,
|
||||
name_arabic, issue_date, expiry_date, license_categories,
|
||||
address, licenseIssueDate, status, birthdate, site,
|
||||
first_name, last_name, accountBank, bankCode,
|
||||
employmentType, maritalStatus, fullNameMaritial, expirationDate,
|
||||
created_at, updated_at
|
||||
) VALUES (
|
||||
:id, :phone, :email, :pwd, :gender, :license_type, :national_number,
|
||||
:name_arabic, :issue_date, :expiry_date, :license_categories,
|
||||
:address, :licenseIssueDate, :status, :birthdate, :site,
|
||||
:first_name, :last_name, :accountBank, :bankCode,
|
||||
:employmentType, :maritalStatus, :fullNameMaritial, :expirationDate,
|
||||
NOW(), NOW()
|
||||
)
|
||||
";
|
||||
$insD = $con->prepare($sqlDriver);
|
||||
$insD->execute([
|
||||
':id' => $driverId,
|
||||
':phone' => $encPhone,
|
||||
':email' => $encEmail,
|
||||
':pwd' => $passwordHashed,
|
||||
':gender' => $encGender,
|
||||
':license_type' => $license_type,
|
||||
':national_number' => $encNationalNumber,
|
||||
':name_arabic' => $encNameArabic,
|
||||
':issue_date' => '2020-01-01',
|
||||
':expiry_date' => '2030-01-01',
|
||||
':license_categories' => 'B',
|
||||
':address' => $encSite,
|
||||
':licenseIssueDate' => '2020-01-01',
|
||||
':status' => 'pending_review',
|
||||
':birthdate' => $encBirthdate,
|
||||
':site' => $encSite,
|
||||
':first_name' => $encFirstName,
|
||||
':last_name' => $encLastName,
|
||||
':accountBank' => 'yet',
|
||||
':bankCode' => 'CIB',
|
||||
':employmentType' => $employmentType,
|
||||
':maritalStatus' => 'Single',
|
||||
':fullNameMaritial' => '',
|
||||
':expirationDate' => date('Y-m-d', strtotime('+5 years')),
|
||||
]);
|
||||
|
||||
/* ================== 2) إدراج السيارة ================== */
|
||||
$sqlCar = "
|
||||
INSERT INTO CarRegistration (
|
||||
driverID, vin, car_plate, make, model, year, expiration_date,
|
||||
color, owner, color_hex, fuel,
|
||||
vehicle_category_id, fuel_type_id,
|
||||
isDefault, created_at, status
|
||||
) VALUES (
|
||||
:driverID, :vin, :car_plate, :make, :model, :year, :expiration_date,
|
||||
:color, :owner, :color_hex, :fuel,
|
||||
:vehicle_category_id, :fuel_type_id,
|
||||
:isDefault, NOW(), 'active'
|
||||
)
|
||||
";
|
||||
$insC = $con->prepare($sqlCar);
|
||||
$insC->execute([
|
||||
':driverID' => $driverId,
|
||||
':vin' => $encVin,
|
||||
':car_plate' => $encCarPlate,
|
||||
':make' => $make,
|
||||
':model' => $model,
|
||||
':year' => $year,
|
||||
':expiration_date' => $expiration_date,
|
||||
':color' => $color,
|
||||
':owner' => $encOwner,
|
||||
':color_hex' => $color_hex,
|
||||
':fuel' => $fuel,
|
||||
':vehicle_category_id' => 1,
|
||||
':fuel_type_id' => 1,
|
||||
':isDefault' => 1,
|
||||
]);
|
||||
|
||||
$carRegID = $con->lastInsertId();
|
||||
|
||||
/* ================== 3) توكن السائق ================== */
|
||||
$token = bin2hex(random_bytes(20));
|
||||
$sqlToken = "
|
||||
INSERT INTO driverToken (token, captain_id, fingerPrint, created_at)
|
||||
VALUES (:token, :captain_id, :fingerPrint, NOW())
|
||||
";
|
||||
$con->prepare($sqlToken)->execute([
|
||||
':token' => $token,
|
||||
':captain_id' => $driverId,
|
||||
':fingerPrint' => 'test_fingerprint',
|
||||
]);
|
||||
|
||||
/* ================== 4) توثيق رقم الهاتف ================== */
|
||||
$sqlPhoneVer = "
|
||||
INSERT INTO phone_verification (phone_number, driverId, email, token_code, expiration_time, is_verified, created_at)
|
||||
VALUES (:phone, :driverId, :email, :token_code, DATE_ADD(NOW(), INTERVAL 1 YEAR), 1, NOW())
|
||||
";
|
||||
$con->prepare($sqlPhoneVer)->execute([
|
||||
':phone' => $encPhone,
|
||||
':driverId' => $driverId,
|
||||
':email' => $encEmail,
|
||||
':token_code' => $encryptionHelper->encryptData('999'),
|
||||
]);
|
||||
|
||||
/* ================== Commit ================== */
|
||||
$con->commit();
|
||||
|
||||
printSuccess([
|
||||
'driverID' => $driverId,
|
||||
'carRegID' => $carRegID,
|
||||
'status' => 'success',
|
||||
'message' => "Driver $first_name $last_name created successfully with status pending_review.",
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
if (isset($con) && $con instanceof PDO && $con->inTransaction()) {
|
||||
$con->rollBack();
|
||||
}
|
||||
error_log("[test_add_driver] " . $e->getMessage());
|
||||
jsonError($e->getMessage());
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
<?php
|
||||
// test_signed_pricing.php
|
||||
// Mock parameters and verify price token generation and booking verification.
|
||||
|
||||
define('TESTING_BYPASS_AUTH', true);
|
||||
|
||||
// Set mock POST parameters for pricing estimation
|
||||
$_POST['distance'] = "10.5";
|
||||
$_POST['durationToRide'] = "1200"; // 20 minutes
|
||||
$_POST['passenger_id'] = "12345";
|
||||
$_POST['country'] = "Syria";
|
||||
$_POST['passengerLat'] = "33.5138";
|
||||
$_POST['passengerLng'] = "36.2765";
|
||||
$_POST['destLat'] = "33.5200";
|
||||
$_POST['destLng'] = "36.2800";
|
||||
$_POST['startNameAddress'] = "Malki, Damascus";
|
||||
$_POST['endNameAddress'] = "Abu Rummaneh, Damascus";
|
||||
$_POST['carType'] = "Speed";
|
||||
|
||||
echo "=== MOCKING PRICING ESTIMATION (get.php) ===\n";
|
||||
|
||||
ob_start();
|
||||
include __DIR__ . '/ride/pricing/get.php';
|
||||
$responseJson = ob_get_clean();
|
||||
|
||||
echo "Response received:\n" . $responseJson . "\n\n";
|
||||
|
||||
$response = json_decode($responseJson, true);
|
||||
if (!$response || $response['status'] !== 'success' || empty($response['price_token'])) {
|
||||
echo "❌ FAILED: Pricing token was not generated successfully.\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$priceToken = $response['price_token'];
|
||||
$estimatedPrices = $response['data'];
|
||||
echo "✅ SUCCESS: Generated price_token successfully!\n";
|
||||
echo "Estimated Speed price: " . $estimatedPrices['totalPassengerSpeed'] . "\n\n";
|
||||
|
||||
// Test 1: Valid Booking with Token
|
||||
echo "=== TEST 1: Booking with authentic token and coordinates ===\n";
|
||||
$_POST['start_location'] = "33.5138, 36.2765";
|
||||
$_POST['end_location'] = "33.5200, 36.2800";
|
||||
$_POST['price'] = "99999.00"; // Client attempts to send garbage price, server must override it!
|
||||
$_POST['price_token'] = $priceToken;
|
||||
$_POST['passenger_id'] = "12345";
|
||||
$_POST['carType'] = "Speed";
|
||||
$_POST['status'] = "waiting";
|
||||
|
||||
// Mock other fields for add_ride.php to prevent errors
|
||||
$_POST['passenger_name'] = "Hamza";
|
||||
$_POST['passenger_phone'] = "+963999999999";
|
||||
$_POST['passenger_token'] = "mock_fcm_token";
|
||||
$_POST['passenger_email'] = "hamza@siromove.com";
|
||||
$_POST['passenger_wallet'] = "0";
|
||||
$_POST['passenger_rating'] = "5.0";
|
||||
$_POST['start_name'] = "Malki";
|
||||
$_POST['end_name'] = "Abu Rummaneh";
|
||||
$_POST['duration_text'] = "20 min";
|
||||
$_POST['distance_text'] = "10.5 km";
|
||||
$_POST['is_wallet'] = "false";
|
||||
$_POST['has_steps'] = "false";
|
||||
|
||||
ob_start();
|
||||
include __DIR__ . '/ride/rides/add_ride.php';
|
||||
$bookingJson = ob_get_clean();
|
||||
|
||||
echo "Booking response:\n" . $bookingJson . "\n\n";
|
||||
$bookingRes = json_decode($bookingJson, true);
|
||||
|
||||
if ($bookingRes && $bookingRes['status'] === 'success') {
|
||||
echo "✅ TEST 1 PASSED: Booking succeeded and overrode client fare!\n";
|
||||
} else {
|
||||
echo "❌ TEST 1 FAILED: Booking rejected valid token.\n";
|
||||
}
|
||||
|
||||
// Test 2: Booking with Tampered Coordinates
|
||||
echo "=== TEST 2: Booking with mismatched start location coordinates ===\n";
|
||||
$_POST['start_location'] = "34.5000, 36.2000"; // Changed start location
|
||||
$_POST['price'] = "99999.00";
|
||||
$_POST['price_token'] = $priceToken;
|
||||
|
||||
ob_start();
|
||||
include __DIR__ . '/ride/rides/add_ride.php';
|
||||
$tamperedJson = ob_get_clean();
|
||||
|
||||
echo "Tampered response:\n" . $tamperedJson . "\n\n";
|
||||
$tamperedRes = json_decode($tamperedJson, true);
|
||||
|
||||
if ($tamperedRes && $tamperedRes['status'] === 'failure' && strpos($tamperedRes['message'], 'route mismatch') !== false) {
|
||||
echo "✅ TEST 2 PASSED: Successfully detected coordinates mismatch and rejected booking!\n";
|
||||
} else {
|
||||
echo "❌ TEST 2 FAILED: Did not correctly reject mismatched coordinates.\n";
|
||||
}
|
||||
?>
|
||||
704
docs/دراسة_نظام_أتمتة_السوق_الذكي.md
Normal file
704
docs/دراسة_نظام_أتمتة_السوق_الذكي.md
Normal file
@@ -0,0 +1,704 @@
|
||||
# دراسة نظام أتمتة السوق الذكي - Siro
|
||||
|
||||
**التاريخ: 26 يونيو 2026**
|
||||
**إعداد: فريق التطوير**
|
||||
|
||||
---
|
||||
|
||||
## فهرس المحتويات
|
||||
|
||||
1. [توحيد السناك بار (Snackbar System)](#1-توحيد-السناك-بار-snackbar-system)
|
||||
2. [نظرة عامة على نظام أتمتة السوق الذكي](#2-نظرة-عامة-على-نظام-أتمتة-السوق-الذكي)
|
||||
3. [مكونات النظام بالتفصيل](#3-مكونات-النظام-بالتفصيل)
|
||||
4. [تدفق البيانات (Data Flow)](#4-تدفق-البيانات-data-flow)
|
||||
5. [قاعدة البيانات وجداولها](#5-قاعدة-البيانات-وجدولها)
|
||||
6. [خريطة مفاتيح Redis](#6-خريطة-مفاتيح-redis)
|
||||
7. [الخدمات الخارجية المستخدمة](#7-الخدمات-الخارجية-المستخدمة)
|
||||
8. [ملفات الخلفية - مراجعة أمنية شاملة](#8-ملفات-الخلفية---مراجعة-أمنية-شاملة)
|
||||
9. [قائمة الملفات المطلوب حذفها فوراً](#9-قائمة-الملفات-المطلوب-حذفها-فوراً)
|
||||
10. [ثغرات SQL Injection](#10-ثغرات-sql-injection)
|
||||
11. [التوصيات النهائية](#11-التوصيات-النهائية)
|
||||
|
||||
---
|
||||
|
||||
## 1. توحيد السناك بار (Snackbar System)
|
||||
|
||||
### الوضع الحالي - 4 تطبيقات و 4 طرق مختلفة
|
||||
|
||||
يوجد حاليًا 4 تطبيقات Flutter ولكل منها نظام سناك بار مختلف تمامًا:
|
||||
|
||||
---
|
||||
|
||||
### 1.1 siro_admin - الإصدار القديم (GetX)
|
||||
|
||||
**الملف:** `siro_admin/lib/views/widgets/snackbar.dart`
|
||||
|
||||
- يستخدم `Get.snackbar()` حصريًا
|
||||
- يوجد دالتان فقط: `mySnackeBarError()` و `mySnackbarSuccess()`
|
||||
- **لا يوجد** `mySnackbarWarning()` ولا `mySnackbarInfo()`
|
||||
- الألوان: أحمر للخطأ (`AppColor.redColor`)، أخضر للنجاح (`AppColor.greenColor`)
|
||||
- يستخدم `SnackbarConfig` للثوابت (مدة 3 ثوان، زوايا 12، ظل)
|
||||
- التوقيع: `SnackbarController mySnackeBarError(String message)`
|
||||
- النصوص بالإنكليزية: `'Error'.tr`, `'Success'.tr`
|
||||
- 30+ استخدامًا في التطبيق
|
||||
|
||||
### 1.2 siro_service - الإصدار القديم (GetX)
|
||||
|
||||
**الملف:** `siro_service/lib/views/widgets/mycircular.dart`
|
||||
|
||||
- يستخدم `Get.snackbar()` حصريًا
|
||||
- يوجد 3 دوال: `mySnackbarError()` و `mySnackbarWarning()` و `mySnackbarSuccess()`
|
||||
- الألوان: أحمر (`AppColor.redColor`)، أصفر (`AppColor.yellowColor` + نص أسود)، أخضر (`AppColor.greenColor`)
|
||||
- نفس `SnackbarConfig` للثوابت
|
||||
- 70+ استخدامًا في التطبيق
|
||||
|
||||
### 1.3 siro_driver - الإصدار الحديث (Custom Widget)
|
||||
|
||||
**الملف:** `siro_driver/lib/views/widgets/error_snakbar.dart`
|
||||
|
||||
- ويج محسّن مخصص مع `_SnackContent` و `AnimationController`
|
||||
- يوجد 4 دوال: `mySnackbarSuccess()` و `mySnackeBarError()` و `mySnackbarInfo()` و `mySnackbarWarning()`
|
||||
- **4 متغيرات (variants):** success, error, info, warning
|
||||
- ألوان: أخضر (`#1A9E5C`)، أحمر (`#D93025`)، أزرق (`#1A73E8`)، برتقالي (`#F29900`)
|
||||
- ألوان السطح: `#F0FBF5`, `#FEF2F1`, `#F0F6FF`, `#FFF8E6`
|
||||
- تأثيرات: `ScaleTransition` مع `Curves.elasticOut`، شريط تقدم تنازلي
|
||||
- إخفاء يدوي مع زر Close + Haptic Feedback
|
||||
- يتحقق أولاً من `Overlay`، فإن لم يجده يستخدم `ScaffoldMessenger`
|
||||
- **مشكلة:** يستخدم `Get.snackbar()` الذي يرمي `FlutterError` إذا لم يكن `Overlay` جاهزًا
|
||||
- 100+ استخدامًا في التطبيق
|
||||
|
||||
### 1.4 siro_rider - الإصدار الحديث المحسّن (ScaffoldMessenger)
|
||||
|
||||
**الملف:** `siro_rider/lib/views/widgets/error_snakbar.dart`
|
||||
|
||||
- نفس تصميم `siro_driver` مع تحسينات جوهرية
|
||||
- **يستخدم فقط `ScaffoldMessenger`** بدلاً من `Get.snackbar()` (لتجنب أخطاء `Overlay`)
|
||||
- يحتوي على آلية إعادة محاولة (retry) تصل إلى 3 مرات
|
||||
- `messenger.clearSnackBars()` قبل العرض (يمنع التراكم)
|
||||
- يعيد `SnackbarController?` (nullable) لأن `Get.snackbar` لم يعد مستخدمًا
|
||||
- التوقيع: `SnackbarController? mySnackbarSuccess(String message)` - ملاحظة: الرجوع `?`
|
||||
- 100+ استخدامًا في التطبيق
|
||||
|
||||
### 1.5 دوال Toast البسيطة
|
||||
|
||||
- **siro_driver** و **siro_rider** لديهما `lib/controller/functions/toast.dart`
|
||||
- `Toast.show(BuildContext context, String message, Color color)` - دالة بسيطة جدًا
|
||||
- تستخدم `ScaffoldMessenger.of(context).showSnackBar()` مع `SnackBar` مادة
|
||||
- 10+ استخدامات في كل تطبيق
|
||||
|
||||
---
|
||||
|
||||
### الفروقات بين الإصدارين (القديم والحديث)
|
||||
|
||||
| الخاصية | siro_admin (قديم) | siro_driver (حديث) | siro_rider (حديث) | siro_service (قديم) |
|
||||
|---------|:-----------------:|:------------------:|:-----------------:|:-------------------:|
|
||||
| عدد الدوال | 2 | 4 | 4 | 3 |
|
||||
| `mySnackbarWarning` | ❌ | ✅ | ✅ | ✅ |
|
||||
| `mySnackbarInfo` | ❌ | ✅ | ✅ | ❌ |
|
||||
| `mySnackbarSuccess` | ✅ | ✅ | ✅ | ✅ |
|
||||
| `mySnackeBarError` | ✅ | ✅ | ✅ | ✅ |
|
||||
| آلية العرض | `Get.snackbar()` | `Get.snackbar()` + `ScaffoldMessenger` | `ScaffoldMessenger` فقط | `Get.snackbar()` |
|
||||
| التصميم | نص فقط | أيقونة + نص + زر إغلاق + شريط تقدم | أيقونة + نص + زر إغلاق + شريط تقدم | نص فقط |
|
||||
| Retry | ❌ | ❌ | ✅ (3 مرات) | ❌ |
|
||||
| Null Safety | `SnackbarController` | `SnackbarController` | `SnackbarController?` | `SnackbarController` |
|
||||
| أخطاء `Overlay` | ✅ (GetX آمن) | ⚠️ (قد يحدث) | ✅ (متفادي) | ✅ (GetX آمن) |
|
||||
|
||||
---
|
||||
|
||||
### خطة التوحيد المقترحة
|
||||
|
||||
#### الهدف: إنشاء package مشترك واحد لجميع التطبيقات الأربعة
|
||||
|
||||
**الخطوة 1:** إنشاء مجلد مشترك (shared package) في المسار:
|
||||
```
|
||||
siro_admin/lib/shared/widgets/snackbar/
|
||||
```
|
||||
|
||||
**الخطوة 2:** توحيد الواجهة (API) لتصبح:
|
||||
|
||||
```dart
|
||||
// الاستخدام الموحد - نفس التوقيع في كل التطبيقات
|
||||
void mySnackbarSuccess(String message);
|
||||
void mySnackbarError(String message);
|
||||
void mySnackbarWarning(String message);
|
||||
void mySnackbarInfo(String message);
|
||||
```
|
||||
|
||||
**الخطوة 3:** الاعتماد على `ScaffoldMessenger` فقط (مثل siro_rider) لتجنب مشاكل `Overlay` في GetX.
|
||||
|
||||
**الخطوة 4:** توحيد الألوان والثوابت:
|
||||
|
||||
| المتغير | اللون الأساسي | لون السطح | الأيقونة |
|
||||
|---------|:------------:|:---------:|:--------:|
|
||||
| success | `#1A9E5C` (أخضر) | `#F0FBF5` | `check_circle_rounded` |
|
||||
| error | `#D93025` (أحمر) | `#FEF2F1` | `error_rounded` |
|
||||
| info | `#1A73E8` (أزرق) | `#F0F6FF` | `info_rounded` |
|
||||
| warning | `#F29900` (برتقالي) | `#FFF8E6` | `warning_amber_rounded` |
|
||||
|
||||
**الخطوة 5:** تحويل ملفات `toast.dart` في `siro_driver` و `siro_rider` لاستخدام دالة واحدة موحدة بدلاً من التكرار.
|
||||
|
||||
**الخطوة 6:** إزالة دوال `Get.snackbar()` المباشرة من جميع ملفات التحكم (controllers) والاستعاضة عنها بالدوال الموحدة.
|
||||
|
||||
---
|
||||
|
||||
## 2. نظرة عامة على نظام أتمتة السوق الذكي
|
||||
|
||||
### ما هو النظام؟
|
||||
|
||||
**"أتمتة السوق الذكي"** هو نظام متكامل لذكاء السوق والتسعير الديناميكي في Siro. يقوم النظام بـ:
|
||||
|
||||
1. **جمع بيانات المنافسين** تلقائيًا عبر Android Bot
|
||||
2. **تحليل الفجوات السعرية** بين Siro والمنافسين (YallaGo, Zaken, Tufaddal)
|
||||
3. **تعديل أسعار Siro تلقائيًا** بناءً على تحليل السوق
|
||||
4. **كشف فرص رفع الأسعار** (Surge Opportunities) في المناطق التي يرتفع فيها الطلب
|
||||
5. **توليد حملات تسويقية ذكية** باستخدام AI (Google Gemini)
|
||||
6. **تقديم تقارير أسبوعية** عن صحة السوق (Market Health Reports)
|
||||
7. **محاكاة "What-If"** لمعرفة أثر تغيير الأسعار
|
||||
|
||||
### طبقات النظام
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ طبقة العرض (Admin Dashboard) │
|
||||
│ siro_admin → لوحة تحكم المدير → Flutter Web │
|
||||
│ endpoints: Admin/marketing/*.php │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ طبقة API (PHP Backend) │
|
||||
│ cron_jobs → bot/*.php │ pricing → ride/pricing/*.php │
|
||||
│ heatmap → ride/heatmap/*.php │ marketing → Admin/marketing/*.php │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ طبقة الذكاء والتحليل │
|
||||
│ Google Gemini AI │ SiroGeminiService │ Redis Analytics │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ طبقة جمع البيانات │
|
||||
│ Android Bot ←→ worker.php ←→ competitor_prices (MySQL) │
|
||||
│ generate_price_tasks.php (cron) → Redis Queue → Bot │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ قاعدة البيانات والذاكرة المؤقتة │
|
||||
│ MySQL (main, tracking, ride) │ Redis (مفاتيح surge, demand) │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### الدول المدعومة
|
||||
|
||||
| الدولة | رمز البلد | المنطقة الزمنية | عملة |
|
||||
|:------:|:--------:|:--------------:|:----:|
|
||||
| سوريا | SY | Asia/Damascus | ل.س |
|
||||
| الأردن | JO | Asia/Amman | د.أ |
|
||||
| مصر | EG | Africa/Cairo | ج.م |
|
||||
| العراق | IQ | Asia/Baghdad | د.ع |
|
||||
|
||||
---
|
||||
|
||||
## 3. مكونات النظام بالتفصيل
|
||||
|
||||
### 3.1 نظام البوت (Bot System) - `backend/bot/`
|
||||
|
||||
#### 3.1.1 `generate_price_tasks.php`
|
||||
- **الجدولة:** كل 15 دقيقة
|
||||
- **الوظيفة:** يولد مهام فحص أسعار المنافسين ويدفعها إلى Redis Queue
|
||||
- **آلية العمل:**
|
||||
- ينشئ جدول `competitor_prices` تلقائيًا إذا لم يكن موجودًا
|
||||
- يحتوي على 10 مناطق رئيسية في دمشق (ساحة الأمويين، المزة، المالكي، كفرسوسة، الميدان، باب توما، ركن الدين، دمر، برامكة، المهاجرين)
|
||||
- المنافسون: `['yallago', 'zaken', 'tufaddal']`
|
||||
- لكل منطقة يولد نقطة انطلاق عشوائية ضمن 2km، ثم رحلة قصيرة (2-5km) وأخرى طويلة (10-15km)
|
||||
- يضغط المهام في Redis list: `queue:bot:tasks`
|
||||
- **الملفات المكتوبة:** Redis key `queue:bot:tasks`
|
||||
- **ملاحظة:** إنشاء الجدول داخل cron job أمر غير محبذ - يجب أن يكون في migration
|
||||
|
||||
#### 3.1.2 `worker.php`
|
||||
- **الوظيفة:** نقطة نهاية API لبوت Android لسحب المهام وإرسال النتائج
|
||||
- **الأمان:** HMAC-SHA256 مع نافذة 5 دقائق + `BOT_SECRET_KEY` من البيئة
|
||||
- **الأجهزة المسموحة:** `['SHAM_CASH_BOT_01', 'PRICE_SCRAPER_BOT_01']`
|
||||
- **GET:** يسحب مهمة من قائمة Redis (`RPOP`)
|
||||
- **POST:** يستقبل النتيجة:
|
||||
- `price_check`: يحسب `pricePerKm` ويدرج في جدول `competitor_prices` و Redis `competitor:price_history`
|
||||
- `payment`: يسجل نجاح الدفع
|
||||
- `failed`: يسجل الخطأ
|
||||
- **Redis:** `queue:bot:tasks` (قراءة)، `competitor:price_history:{app}` (كتابة)
|
||||
- **MySQL:** إدراج في `competitor_prices`
|
||||
|
||||
#### 3.1.3 `standalone_worker.php`
|
||||
- **الوظيفة:** نسخة بديلة كاملة بذاتها بدون Redis أو MySQL - تستخدم ملفات JSON
|
||||
- **الاستخدام:** اختبار محلي / تطوير
|
||||
- **الميزات:** لوحة تحكم HTML داكنة مع Bootstrap، منشئ مهام، سجل مهام
|
||||
- **الأمان:** HMAC-SHA256 مع نافذة 15 دقيقة
|
||||
- **لا يشكل خطرًا أمنيًا في الإنتاج طالما لا يمكن الوصول إليه عبر الويب**
|
||||
|
||||
#### 3.1.4 `cron_surge_opportunity.php` (قلب النظام)
|
||||
- **الجدولة:** كل 10 دقائق
|
||||
- **الوظيفة:** محرك كشف فرص رفع الأسعار (Surge Detection)
|
||||
- **آلية العمل:**
|
||||
1. يستعلم من `competitor_prices` ويجمّع البيانات بخلايا جغرافية (~1.5km)
|
||||
2. يحسب خط الأساس (baseline): متوسط سعر كل منافس لآخر 7 أيام
|
||||
3. يحسب السعر الحالي: متوسط لكل منافس في آخر ساعتين
|
||||
4. يشترط وجود 2 عينة على الأقل لكل منافس في كل خلية
|
||||
5. يكتشف الفرصة عندما **جميع** المنافسين في الخلية رفعوا أسعارهم
|
||||
6. يقترح مضاعف السعر: `1.0 + (avg_competitor_surge_ratio - 1.0) * 0.6` (يقلل عن المنافسين بـ 40%)
|
||||
7. يحفظ في Redis: `surge:opportunities` مع TTL 10 دقائق
|
||||
- **SQL الرئيسي:**
|
||||
```sql
|
||||
SELECT ROUND(lat * 74) / 74 AS lat_group, ROUND(lng * 74) / 74 AS lng_group,
|
||||
competitor_name, country_code,
|
||||
AVG(CASE WHEN created_at < DATE_SUB(NOW(), INTERVAL 6 HOUR)
|
||||
THEN price_per_km END) AS baseline_avg,
|
||||
AVG(CASE WHEN created_at >= DATE_SUB(NOW(), INTERVAL 2 HOUR)
|
||||
THEN price_per_km END) AS current_avg,
|
||||
COUNT(CASE WHEN created_at >= DATE_SUB(NOW(), INTERVAL 2 HOUR)
|
||||
THEN 1 END) AS recent_samples
|
||||
FROM competitor_prices
|
||||
GROUP BY lat_group, lng_group, competitor_name, country_code
|
||||
HAVING recent_samples >= 2
|
||||
```
|
||||
|
||||
#### 3.1.5 `cron_kazan_adjuster.php`
|
||||
- **الجدولة:** كل 10 دقائق
|
||||
- **الوظيفة:** يخفف عمولة Siro (Kazan) في المناطق التي يرتفع فيها سعر المنافسين
|
||||
- **آلية العمل:**
|
||||
- يقرأ `surge:opportunities:{country}` من Redis
|
||||
- للخلايا ذات surge > 1.2: يطبق تخفيض 30% على العمولة (0.70x)
|
||||
- للخلايا ذات surge 1.05-1.2: يطبق تخفيض 15% (0.85x)
|
||||
- يحفظ في Redis: `surge:kazan_discounts:{country}` مع TTL 20 دقيقة
|
||||
- **ملاحظة:** يستدعي دالة `getRedisConnection()` غير المعرّفة - سيفشل في وقت التشغيل
|
||||
|
||||
#### 3.1.6 `cron_seasonal_pricing.php`
|
||||
- **الجدولة:** كل 30-60 دقيقة
|
||||
- **الوظيفة:** يطبق مضاعفات موسمية (رمضان، عيد، طقس سيء)
|
||||
- **القواعد:**
|
||||
- `ramadan_iftar` (18:00-20:00): 1.25x
|
||||
- `eid`: 1.15x (غير نشط حاليًا)
|
||||
- `severe_weather`: 1.30x (غير نشط حاليًا)
|
||||
- **ملاحظة:** نفس مشكلة `getRedisConnection()` غير المعرّفة
|
||||
|
||||
#### 3.1.7 `cron_weekly_health_report.php`
|
||||
- **الجدولة:** أسبوعيًا (ليلة الأحد)
|
||||
- **الوظيفة:** ينشئ تقرير صحة السوق الأسبوعي
|
||||
- **المقاييس:**
|
||||
- **PCI (Price Competitiveness Index):** `siroPrice / compPrice`
|
||||
- **حصة السوق:** % من الرحلات التي Siro فيها أرخص
|
||||
- **عدد الشذوذ (anomalies):** من جدول `price_anomalies`
|
||||
- **عدد الحملات:** من جدول `marketing_campaigns_log`
|
||||
- **Redis:** لا يستخدم
|
||||
- **MySQL:** يدرج في `market_health_reports`
|
||||
|
||||
---
|
||||
|
||||
### 3.2 نظام التسعير الديناميكي - `backend/ride/pricing/`
|
||||
|
||||
#### 3.2.1 `auto_adapt.php`
|
||||
- **الجدولة:** كل 30-60 دقيقة
|
||||
- **الوظيفة:** يعدل جميع أسعار Siro بناءً على أدنى متوسط سعر للمنافسين
|
||||
- **المعادلة:** `new_price_per_km = lowest_competitor_avg * 0.92` (أقل من المنافسين بـ 8%)
|
||||
- **النطاق:** بين 85% و 115% من السعر الحالي
|
||||
- **الأعمدة المحدثة:** speedPrice, comfortPrice, ladyPrice, electricPrice, vanPrice, deliveryPrice, mishwarVipPrice, fixedPrice, awfarPrice
|
||||
- **الدول:** SY, JO, EG, IQ
|
||||
- **SQL الرئيسي:**
|
||||
```sql
|
||||
SELECT competitor_name, AVG(price_per_km) AS avg_ppm
|
||||
FROM competitor_prices
|
||||
WHERE country_code = :cc AND created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
|
||||
AND price_per_km > 0
|
||||
GROUP BY competitor_name
|
||||
ORDER BY avg_ppm ASC
|
||||
```
|
||||
|
||||
#### 3.2.2 `get.php` (نقطة تسعير الرحلة - 511 سطرًا)
|
||||
- **الوظيفة:** يحسب سعر الرحلة عند الطلب (أثناء تشغيل التطبيق)
|
||||
- **آلية العمل المعقدة:**
|
||||
1. **حساب الخلية الجغرافية:** من إحداثيات الراكب (~1.5km)
|
||||
2. **قراءة الطلب:** `$redis->get("demand:grid:" . $grid_id)`
|
||||
3. **قراءة surge المنافسين:** من `surge:opportunities` في Redis
|
||||
4. **حساب توفر السائقين:** `$redisLocation->georadius()` ضمن 0.75km
|
||||
5. **Surge Calculation:** إذا `طلب/سائقين > 1.2`، يطبق مضاعف يصل إلى 3.0x
|
||||
6. **التسعير الزمني:** الليل (21:00-01:00) ← `latePrice`، الفجر (01:00-05:00) ← مضاعف، الظهر (14:00-17:00) ← `heavyPrice`
|
||||
7. **تخفيضات المسافات الطويلة:** 40km+ و 100km+
|
||||
8. **مطابقة المنافسين:** مستويين من المطابقة:
|
||||
- المستوى 1: يطابق نقطة البداية والنهاية معًا
|
||||
- المستوى 2: يطابق نقطة البداية فقط
|
||||
- يخفض السعر بنسبة 8% إذا كان السعر المحسوب أعلى من متوسط المنافسين
|
||||
9. **العمولة:** `price * (1 + kazanPercent / 100)`
|
||||
10. **التحقق من العروض:** يتحقق من جدول `promos`
|
||||
11. **ديون الراكب:** يقرأ من Redis `passenger_debt_{id}`
|
||||
12. **توليد توكن:** يشفر حمولة السعر بانتهاء صلاحية 7 دقائق لمنع التلاعب
|
||||
- **المفاتيح الأساسية:** `kazan`, `promos`, `competitor_prices`
|
||||
- **Redis:** `demand:grid:{id}`, `surge:opportunities`, `passenger_debt_{id}`
|
||||
|
||||
---
|
||||
|
||||
### 3.3 نظام الخريطة الحرارية (Heatmap) - `backend/ride/heatmap/`
|
||||
|
||||
#### 3.3.1 `log_demand.php`
|
||||
- **الوظيفة:** يسجل الطلب عند كل خلية جغرافية
|
||||
- **Redis:** `INCR` على `demand:grid:{grid_id}` مع TTL 60 ثانية
|
||||
- **يُستدعى من:** تطبيق الراكب عند طلب رحلة
|
||||
|
||||
#### 3.3.2 `get_surge_heatmap.php`
|
||||
- **الوظيفة:** يعرض بؤر surge لتطبيق السائق (Driver App)
|
||||
- **المصادقة:** يتطلب دور `driver`
|
||||
- **Redis:** يقرأ `surge:opportunities:{countryCode}`
|
||||
- **الفلترة:** فقط الخلايا ذات مضاعف > 1.05
|
||||
|
||||
#### 3.3.3 `heatmap_live.php`
|
||||
- **الوظيفة:** خريطة حية لتطبيق الكابتن - تجمع بين الطلب وتوفر السائقين
|
||||
- **تحذير:** يستخدم `$redis->keys("demand:grid:*")` - أمر `KEYS` بطيء مع عدد كبير من المفاتيح
|
||||
- **التصنيف:** عالي (نسبة > 2.0 أو عدد ≥ 5)، متوسط (نسبة > 1.2 أو عدد ≥ 3)، منخفض
|
||||
|
||||
---
|
||||
|
||||
### 3.4 نقط نهاية التسويق - `backend/Admin/marketing/`
|
||||
|
||||
#### 3.4.1 `trigger_campaign.php` (200 سطر - الأكثر تعقيدًا)
|
||||
- **الوظيفة:** يطلق حملة تسويقية مدعومة بالذكاء الاصطناعي
|
||||
- **الخطوات:**
|
||||
1. يجلب آخر 10 أسعار منافسين من MySQL
|
||||
2. يستدعي `SiroGeminiService->analyzeMarketAndDraftCampaign()` مع بيانات السوق
|
||||
3. إذا تم اكتشاف فرصة (`opportunity_detected`):
|
||||
- يجد الركاب المستهدفين من `passenger_opening_locations`
|
||||
- ينشئ كود خصم في جدول `promos` (صالحة 7 أيام)
|
||||
- لكل راكب:
|
||||
- إذا لديه FCM token: يرسل إشعار دفع
|
||||
- إذا لا: يتحقق من anti-spam (24 ساعة)، ثم WhatsApp → SMS
|
||||
4. يسجل في `admin_audit_log`
|
||||
- **الخدمات الخارجية:** Google Gemini, WhatsApp Bot, Firebase Cloud Messaging
|
||||
- **الحماية:** Anti-spam (24h cooldown للـ SMS/WhatsApp)
|
||||
|
||||
#### 3.4.2 `ai_price_prediction.php`
|
||||
- **الوظيفة:** يتوقع ساعات الذروة بناءً على بيانات 14 يومًا
|
||||
- **المنطق:** `SELECT HOUR(created_at) FROM price_anomalies WHERE anomaly_type = 'opportunity' GROUP BY HOUR ORDER BY COUNT(*) DESC LIMIT 3`
|
||||
- **النتيجة:** أفضل 3 ساعات متوقعة + نسبة ثقة 85%
|
||||
|
||||
#### 3.4.3 `what_if_simulator.php`
|
||||
- **الوظيفة:** محاكي "ماذا لو" - يقترح السعر الأمثل
|
||||
- **المنطق:**
|
||||
- يأخذ `speed_price` مقترح
|
||||
- يحاكي السعر لآخر 500 رحلة منافس
|
||||
- يحسب PCI الجديد وحصة السوق المتوقعة
|
||||
- التوصية: PCI < 0.8 تحذير (ربح قليل)، 0.9-0.95 ممتاز، > 1.0 خطر
|
||||
|
||||
#### 3.4.4 `surge_opportunity_index.php`
|
||||
- **الوظيفة:** نسخة Admin من `cron_surge_opportunity.php` - استعلام فوري
|
||||
- **الفرق:** يُستدعى عبر HTTP، يعرض تفاصيل كل منطقة ومنافس
|
||||
|
||||
#### 3.4.5 `winback_hotspot_targets.php`
|
||||
- **الوظيفة:** يجد الركاب الخاملين (30 يوم بدون رحلة) في مناطق surge
|
||||
- **Redis:** يقرأ `surge:opportunities:{countryCode}`
|
||||
- **MySQL:** `users JOIN passenger_opening_locations`
|
||||
|
||||
#### 3.4.6 `get_price_gap_heatmap.php`
|
||||
- **الوظيفة:** خريطة حرارية توضح أين Siro أرخص/أغلى من المنافسين
|
||||
- **المنطق:** لكل خلية جغرافية يحسب `pci = currentSpeedPrice / avg_competitor_price` و `weight = pci - 1.0` مقيد بـ [-1, 1]
|
||||
|
||||
#### 3.4.7 باقي نقاط النهاية
|
||||
| الملف | الوظيفة |
|
||||
|-------|---------|
|
||||
| `get_campaigns_log.php` | سجل الحملات التسويقية |
|
||||
| `get_market_anomalies.php` | الشذوذ السعري + آخر أسعار المنافسين |
|
||||
| `get_market_share_analytics.php` | بيانات حصة السوق للرسوم البيانية (آخر 12 أسبوع) |
|
||||
| `get_price_comparison.php` | مقارنة الأسعار: متوسطات الساعة، PCI حسب المنطقة، أسعار Siro |
|
||||
| `get_telemetry.php` | إحصائيات استخدام النظام: عدد الحملات، التكلفة التقديرية |
|
||||
|
||||
---
|
||||
|
||||
### 3.5 خدمة الذكاء الاصطناعي - `backend/core/Services/SiroGeminiService.php`
|
||||
|
||||
- **النموذج:** `gemini-1.5-flash`
|
||||
- **الوظيفة:** تحليل السوق وكتابة الحملات التسويقية بالعربية
|
||||
- **المخرجات:** JSON يحتوي على `opportunity_detected`, `campaign_text`, `discount_percent`, `message_type`
|
||||
- **التكيف:** يضبط اللهجة حسب البلد (سوري، أردني، مصري، عراقي)
|
||||
|
||||
---
|
||||
|
||||
## 4. تدفق البيانات (Data Flow)
|
||||
|
||||
```
|
||||
generate_price_tasks.php (cron/15min)
|
||||
│
|
||||
│ LPUSH → queue:bot:tasks (Redis)
|
||||
▼
|
||||
Android Bot (Scraper) ←→ worker.php (API)
|
||||
│
|
||||
│ POST result → INSERT competitor_prices
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ competitor_prices (MySQL) │
|
||||
│ country_code | lat | lng | price_per_km | competitor_name │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
├─── cron_surge_opportunity.php (cron/10min)
|
||||
│ │ SELECT baseline vs current
|
||||
│ │ WRITE surge:opportunities (Redis)
|
||||
│ ▼
|
||||
├─── cron_kazan_adjuster.php (cron/10min)
|
||||
│ │ READ surge:opportunities:{CC}
|
||||
│ │ WRITE surge:kazan_discounts:{CC}
|
||||
│ ▼
|
||||
├─── cron_seasonal_pricing.php (cron/30-60min)
|
||||
│ │ WRITE surge:seasonal:{CC}
|
||||
│ ▼
|
||||
├─── auto_adapt.php (cron/30-60min)
|
||||
│ │ SELECT AVG(price_per_km) → UPDATE kazan
|
||||
│ ▼
|
||||
├─── cron_weekly_health_report.php (cron/weekly)
|
||||
│ │ SELECT → INSERT market_health_reports
|
||||
│ ▼
|
||||
├─── ride/pricing/get.php (API - عند طلب رحلة)
|
||||
│ │ READ Redis (surge, demand, debt)
|
||||
│ │ SELECT (competitor_prices, kazan, promos)
|
||||
│ ▼
|
||||
└─── Admin/marketing/*.php (API - لوحة التحكم)
|
||||
│ READ (competitor_prices, anomalies, etc.)
|
||||
│ WRITE (promos, campaigns_log, audit_log)
|
||||
│ CALL Gemini API
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. قاعدة البيانات وجداولها
|
||||
|
||||
| الجدول | العمليات (SELECT) | العمليات (INSERT/UPDATE) |
|
||||
|--------|:-----------------:|:------------------------:|
|
||||
| `competitor_prices` | cron_surge_opportunity, cron_weekly_health, surge_opportunity_index, get_price_comparison, get_price_gap_heatmap, what_if_simulator, trigger_campaign, auto_adapt, pricing/get | worker.php (INSERT), generate_price_tasks (CREATE TABLE) |
|
||||
| `kazan` | cron_weekly_health, get_price_comparison, get_price_gap_heatmap, what_if_simulator, pricing/get | auto_adapt (UPDATE) |
|
||||
| `price_anomalies` | ai_price_prediction, get_market_anomalies, cron_weekly_health, get_telemetry | (خارج نطاق هذه الدراسة) |
|
||||
| `market_health_reports` | get_market_share_analytics | cron_weekly_health (INSERT) |
|
||||
| `marketing_campaigns_log` | get_campaigns_log, cron_weekly_health, get_telemetry | trigger_campaign (INSERT) |
|
||||
| `passenger_opening_locations` | trigger_campaign, winback_hotspot_targets | (خارج النطاق) |
|
||||
| `promos` | pricing/get | trigger_campaign (INSERT) |
|
||||
| `passengers` | get_campaigns_log, trigger_campaign | (خارج النطاق) |
|
||||
| `tokens` | trigger_campaign | (خارج النطاق) |
|
||||
| `users` | winback_hotspot_targets | (خارج النطاق) |
|
||||
| `admin_audit_log` | - | trigger_campaign (INSERT via logAudit) |
|
||||
|
||||
---
|
||||
|
||||
## 6. خريطة مفيكات Redis
|
||||
|
||||
| نمط المفتاح | يُكتب بواسطة | يُقرأ بواسطة | TTL |
|
||||
|:-----------:|:------------:|:------------:|:---:|
|
||||
| `surge:opportunities` | cron_surge_opportunity, surge_opportunity_index | pricing/get, get_surge_heatmap | 600s |
|
||||
| `surge:opportunities:{CC}` | cron_kazan_adjuster | winback_hotspot_targets, get_surge_heatmap | 1200s |
|
||||
| `surge:kazan_discounts:{CC}` | cron_kazan_adjuster | (ride logic) | 1200s |
|
||||
| `surge:seasonal:{CC}` | cron_seasonal_pricing | (ride logic) | 3600s |
|
||||
| `queue:bot:tasks` | generate_price_tasks | worker.php (RPOP) | list |
|
||||
| `competitor:price_history:{app}` | worker.php | (تحليلات مستقبلية) | list/50 |
|
||||
| `demand:grid:{grid}` | log_demand | pricing/get, heatmap_live | 60s |
|
||||
| `passenger_debt_{id}` | (من نظام المحفظة) | pricing/get | متغير |
|
||||
|
||||
---
|
||||
|
||||
## 7. الخدمات الخارجية المستخدمة
|
||||
|
||||
| الخدمة | الاستخدام | الملف المرتبط |
|
||||
|--------|:---------:|:-------------:|
|
||||
| **Google Gemini AI** | إنشاء محتوى الحملات التسويقية | `SiroGeminiService.php`, `trigger_campaign.php` |
|
||||
| **Firebase Cloud Messaging** | إشعارات الدفع للركاب | `trigger_campaign.php`, `FcmService.php` |
|
||||
| **WhatsApp Bot Servers** | إرسال رسائل واتساب | `trigger_campaign.php` |
|
||||
| **Android Bot (Scraper)** | جمع أسعار المنافسين | `worker.php`, `generate_price_tasks.php` |
|
||||
| **Location Socket Server** | مواقع السائقين اللحظية | `heatmap_live.php`, `pricing/get.php` |
|
||||
|
||||
---
|
||||
|
||||
## 8. ملفات الخلفية - مراجعة أمنية شاملة
|
||||
|
||||
### 8.1 ملفات اختبار/تجربة عالية الخطورة (يجب حذفها فورًا)
|
||||
|
||||
#### `backend/test_add_driver_and_car.php`
|
||||
- **الوصف:** سكربت اختبار يُنشئ سائق وسيارة في قاعدة البيانات مباشرة
|
||||
- **الخطر:** لا يتطلب أي مصادقة - أي زائر يمكنه إنشاء حسابات سائقين وهمية
|
||||
- **التصنيف:** **🚨 عالي جدًا - احذف فورًا**
|
||||
|
||||
#### `backend/test_signed_pricing.php`
|
||||
- **الوصف:** سكربت اختبار يتحايل على المصادقة (`define('TESTING_BYPASS_AUTH', true)`)
|
||||
- **الخطر:** يمكنه إنشاء رحلات حقيقية ببيانات مزيفة
|
||||
- **التصنيف:** **🚨 عالي جدًا - احذف فورًا**
|
||||
|
||||
#### `backend/diagnose_fingerprint.php`
|
||||
- **الوصف:** أداة تشخيص تفضي ببيانات البصمات المخزنة
|
||||
- **الخطر:** يعرض البيانات مفككة التشفير بدون مصادقة
|
||||
- **التصنيف:** **🚨 عالي جدًا - احذف فورًا**
|
||||
|
||||
#### `backend/diagnose_login.php`
|
||||
- **الوصف:** أداة تشخيص تسجيل الدخول - تعرض أرقام الهواتف والأسماء
|
||||
- **الخطر:** يعرض PII (معلومات شخصية) مفككة التشفير
|
||||
- **التصنيف:** **🚨 عالي جدًا - احذف فورًا**
|
||||
|
||||
#### `backend/auth/Tester/getTesterApp.php` و `updateTesterApp.php`
|
||||
- **الوصف:** نقاط نهاية اختبار بدون مصادقة مع SQL Injection
|
||||
- **الخطر:** SQL Injection صريح + لا مصادقة
|
||||
- **التصنيف:** **🚨 عالي جدًا - احذف فورًا**
|
||||
|
||||
#### `backend/migration_create_table.php`
|
||||
- **الوصف:** سكربت إنشاء جدول (كان يجب حذفه بعد الاستخدام)
|
||||
- **الخطر:** يمكن إعادة تنفيذه لتعديل schema قاعدة البيانات
|
||||
- **التصنيف:** **🚨 عالي جدًا - احذف فورًا**
|
||||
|
||||
#### `backend/migrate_driver_passwords.php`
|
||||
- **الوصف:** سكربت ترحيل كلمات المرور
|
||||
- **الخطر:** يحتوي على بيانات اعتماد قاعدة بيانات في الكود
|
||||
- **التصنيف:** **🚨 عالي جدًا - احذف فورًا**
|
||||
|
||||
#### `backend/Admin/auth/migration_cryptography.php`
|
||||
- **الوصف:** يعيد تشفير جميع الأعمدة المشفرة (أكثر من 15 جدول)
|
||||
- **الخطر:** إعادة تنفيذه قد تفسد جميع البيانات المشفرة
|
||||
- **التصنيف:** **🚨 عالي - احذف فورًا**
|
||||
|
||||
#### `backend/Admin/auth/migrate_db.php`
|
||||
- **الوصف:** سكربت ترحيل بنية قاعدة البيانات
|
||||
- **الخطر:** يمكنه تعديل schema الإنتاج
|
||||
- **التصنيف:** **🚨 عالي - احذف فورًا**
|
||||
|
||||
#### `backend/Admin/Staff/add_super_admin.php`
|
||||
- **الوصف:** إضافة مشرف (Admin) جديد
|
||||
- **الخطر:** التحقق من الصلاحية معلّق (commented out) - أي زائر يمكنه إنشاء super admin
|
||||
- **التصنيف:** **🚨 عالي جدًا - احذف فورًا**
|
||||
|
||||
#### `backend/Admin/Staff/setup.php`
|
||||
- **الوصف:** سكربت إعداد الموظفين الأولي
|
||||
- **الخطر:** يمكنه إعادة تعيين جميع صلاحيات المشرفين
|
||||
- **التصنيف:** **🚨 عالي - احذف بعد التأكد من الإعداد**
|
||||
|
||||
### 8.2 ملفات ترحيل البصمات - مفتاح مشفر مكشوف
|
||||
|
||||
#### `siro_admin/lib/views/admin/enceypt/fingerprint_migration.dart`
|
||||
#### `siro_admin/lib/views/admin/enceypt/driver_fingerprint_migration.dart`
|
||||
- **الوصف:** أدوات Flutter لترحيل البصمات
|
||||
- **الخطر:** يحتويان على مفتاح المشرف (admin key) بشكل نصي صريح:
|
||||
```
|
||||
'iuyweiruinakjbfkajkjlkmalkcxnlahd'
|
||||
```
|
||||
- **التصنيف:** **🔥 خطير جدًا - احذف فورًا وغيّر المفتاح في السيرفر**
|
||||
|
||||
### 8.3 ملفات "ggg" - أدوات التشفير
|
||||
|
||||
| الملف | الوظيفة | المصادقة | التصنيف |
|
||||
|-------|:-------:|:--------:|:-------:|
|
||||
| `backend/ggg.php` | تشفير/فك تشفير للمشرف | JWT admin/super_admin | ⚠️ متوسط |
|
||||
| `backend/Admin/ggg.php` | تشفير/فك تشفير | ADMIN_PHONE_NUMBERS | ⚠️ متوسط |
|
||||
|
||||
**التوصية:** احذف بعد انتهاء الترحيل. هذه أدوات خطيرة لأنها تسمح بفك تشفير أي بيانات.
|
||||
|
||||
### 8.4 ملفات الترحيل (Migration Files)
|
||||
|
||||
| الملف | الوظيفة | التصنيف |
|
||||
|-------|:-------:|:-------:|
|
||||
| `backend/migration/get_all_fingerprints.php` | تصدير بصمات الركاب | ⚠️ متوسط |
|
||||
| `backend/migration/get_all_driver_fingerprints.php` | تصدير بصمات السائقين | ⚠️ متوسط |
|
||||
| `backend/migration/update_fingerprint_admin.php` | تحديث بصمة راكب | ⚠️ متوسط |
|
||||
| `backend/migration/update_driver_fingerprint_admin.php` | تحديث بصمة سائق | ⚠️ متوسط |
|
||||
|
||||
**التوصية:** احذف جميع ملفات الترحيل بعد التأكد من اكتمال الترحيل في الإنتاج.
|
||||
|
||||
### 8.5 ملفات أخرى
|
||||
|
||||
#### `backend/intaleq_v1_secure_latest.md`
|
||||
- **الوصف:** ملف توثيق ضخم (33,608 سطر) يحتوي على كود المصدر الكامل
|
||||
- **الخطر:** إذا كان الوصول إليه ممكنًا عبر الويب، فإنه يسرب كامل قاعدة الشفرة
|
||||
- **التصنيف:** ⚠️ متوسط - انقل خارج جذر الويب أو احذف
|
||||
|
||||
#### `siro_admin/lib/debug_jwt.dart`
|
||||
- **الوصف:** سكربت Flutter لاختبار JWT
|
||||
- **الخطر:** يكشف بنية JWT ومنهجية التشفير
|
||||
- **التصنيف:** ⚠️ منخفض - احذف من بنية الإنتاج
|
||||
|
||||
---
|
||||
|
||||
## 9. قائمة الملفات المطلوب حذفها فوراً
|
||||
|
||||
### أولوية قصوى - خطر أمني مباشر 🔴
|
||||
|
||||
| الملف | السبب |
|
||||
|:------|:-----:|
|
||||
| `backend/test_add_driver_and_car.php` | إنشاء سائقين بدون مصادقة |
|
||||
| `backend/test_signed_pricing.php` | تجاوز المصادقة |
|
||||
| `backend/diagnose_fingerprint.php` | كشف بيانات حساسة بدون مصادقة |
|
||||
| `backend/diagnose_login.php` | كشف PII بدون مصادقة |
|
||||
| `backend/migration_create_table.php` | تعديل schema بدون مصادقة |
|
||||
| `backend/migrate_driver_passwords.php` | بيانات اعتماد DB في الكود |
|
||||
| `backend/Admin/auth/migration_cryptography.php` | إعادة تشفير شامل |
|
||||
| `backend/Admin/auth/migrate_db.php` | تعديل schema الإنتاج |
|
||||
| `backend/Admin/Staff/add_super_admin.php` | صلاحية المشرِف معلّقة |
|
||||
| `backend/Admin/Staff/setup.php` | إعادة تعيين الصلاحيات |
|
||||
| `backend/auth/Tester/getTesterApp.php` | SQL Injection + لا مصادقة |
|
||||
| `backend/auth/Tester/updateTesterApp.php` | SQL Injection + لا مصادقة |
|
||||
| `siro_admin/lib/views/admin/enceypt/fingerprint_migration.dart` | مفتاح مكشوف 🔥 |
|
||||
| `siro_admin/lib/views/admin/enceypt/driver_fingerprint_migration.dart` | مفتاح مكشوف 🔥 |
|
||||
|
||||
### أولوية متوسطة - أمان أو تنظيف 🟡
|
||||
|
||||
| الملف | السبب |
|
||||
|:------|:-----:|
|
||||
| `backend/ggg.php` | أداة تشفير للمشرفين - احذف بعد الترحيل |
|
||||
| `backend/Admin/ggg.php` | أداة تشفير - احذف بعد الترحيل |
|
||||
| `backend/migration/get_all_fingerprints.php` | ترحيل بصمات - احذف بعد التأكد |
|
||||
| `backend/migration/get_all_driver_fingerprints.php` | ترحيل بصمات - احذف بعد التأكد |
|
||||
| `backend/migration/update_fingerprint_admin.php` | ترحيل بصمات - احذف بعد التأكد |
|
||||
| `backend/migration/update_driver_fingerprint_admin.php` | ترحيل بصمات - احذف بعد التأكد |
|
||||
| `backend/Admin/auth/register.php` | مراجعة - هل ما زال مستخدمًا؟ |
|
||||
| `siro_admin/lib/debug_jwt.dart` | تصحيح JWT - احذف من الإنتاج |
|
||||
| `backend/intaleq_v1_secure_latest.md` (33k سطر) | كود مصدر كامل - انقل أو احذف |
|
||||
|
||||
### أولوية منخفضة - تحسين أمني 🟢
|
||||
|
||||
| الملف | السبب |
|
||||
|:------|:-----:|
|
||||
| `walletintaleq.intaleq.xyz/v2/main/ride/payment/delete.php` | ملف فارغ تمامًا |
|
||||
| `backend/logo.png` | (تحقق ما إذا كان ضروريًا) |
|
||||
|
||||
---
|
||||
|
||||
## 10. ثغرات SQL Injection
|
||||
|
||||
### 10.1 ملفات ذات SQL Injection مباشر
|
||||
|
||||
هذه الملفات تستخدم `prepare()` بعد `$sql = "... WHERE id = '$var'"` مما يبطل تمامًا حماية prepared statements:
|
||||
|
||||
| الملف | السطر | الكود المخترق |
|
||||
|:------|:-----:|:--------------|
|
||||
| `wallet/ride/passengerWallet/delete.php` | `$sql = "DELETE FROM passengerWallet WHERE id = '$id'"` |
|
||||
| `wallet/ride/passengerWallet/update.php` | `$sql = "UPDATE passengerWallet SET balance = '$balance' WHERE id = '$id'"` |
|
||||
| `wallet/ride/passengerWallet/getAllPassengerTransaction.php` | `WHERE passenger_id = '$passenger_id'` |
|
||||
| `wallet/ride/passengerWallet/getPassengerWalletArchive.php` | `WHERE passenger_id = '$passenger_id'` |
|
||||
| `wallet/ride/driverPayment/delete.php` | `DELETE FROM paymentsDriverPoints WHERE id = '$id'` |
|
||||
| `wallet/ride/driverPayment/update.php` | `UPDATE ... SET amount = '$amount', ...` |
|
||||
| `wallet/ride/driverPayment/add.php` | `INSERT INTO paymentsDriverPoints VALUES ('$amount', '$paymentMethod', '$driverID')` |
|
||||
| `wallet/ride/driverWallet/get.php` | `WHERE driverID = '$driverID'` (في subquery) |
|
||||
| `wallet/ride/payment/update.php` | بناء `SET` ديناميكي + SQL Injection |
|
||||
| `backend/Admin/adminUser/get.php` | `WHERE device_number = '$device_number'` |
|
||||
|
||||
### 10.2 الإجراء المطلوب
|
||||
|
||||
1. **فوري:** استبدال `$sql = "... WHERE id = '$var'"` بـ `$sql = "... WHERE id = :id"` مع `bindParam(':id', $id)`
|
||||
2. **مراجعة:** جميع ملفات `walletintaleq/` لديها مشكلة أمنية منهجية - تحتاج مراجعة كاملة
|
||||
3. **تحذير:** ملفات `driverPayment/` لا تملك أي مصادقة JWT - يمكن لأي زوار الوصول إليها
|
||||
|
||||
---
|
||||
|
||||
## 11. التوصيات النهائية
|
||||
|
||||
### أولاً - فوري (خلال 24 ساعة)
|
||||
1. **حذف جميع الملفات عالية الخطورة (القسم 9 - 🔴)**
|
||||
2. **تغيير مفتاح `MIGRATION_ADMIN_KEY`** في جميع السيرفرات (المفتاح `iuyweiruinakjbfkajkjlkmalkcxnlahd` أصبح مكشوفًا)
|
||||
3. **إصلاح ثغرات SQL Injection** في ملفات المحفظة (القسم 10)
|
||||
|
||||
### ثانيًا - قصير المدى (خلال أسبوع)
|
||||
4. **توحيد السناك بار** حسب الخطة في القسم 1
|
||||
5. **حذف ملفات الترحيل** بعد التأكد من اكتمال الترحيل
|
||||
6. **نقل أو حذف `intaleq_v1_secure_latest.md`** من جذر الويب
|
||||
7. **مراجعة جميع ملفات `walletintaleq/`** - معظمها يفتقر إلى مصادقة JWT
|
||||
|
||||
### ثالثًا - طويل المدى (شهر)
|
||||
8. **تبسيط الـ Redis architecture** - حاليًا يوجد اتصالان منفصلان (main + location)
|
||||
9. **استبدال `KEYS` command** في `heatmap_live.php` بـ `SCAN`
|
||||
10. **إصلاح الدوال غير المعرّفة** `getRedisConnection()` و `resolveAdminCountry()`
|
||||
11. **فصل إنشاء الجداول** من ملفات cron إلى migration scripts
|
||||
12. **توسيع التغطية الجغرافية** - حاليًا تتركز على دمشق فقط
|
||||
|
||||
### رابعًا - تحسينات إضافية
|
||||
13. **إضافة مزيد من التوثيق** للـ cron jobs (جدولة، مخرجات، مراقبة)
|
||||
14. **إضافة نظام إنذار** عند فشل أحد cron jobs
|
||||
15. **تشفير جميع مفاتيح API** في ملف `.env` فقط
|
||||
16. **تطبيق pipeline CI/CD** يستبعد ملفات الاختبار والترحيل من الإنتاج
|
||||
|
||||
---
|
||||
|
||||
*تم إعداد هذه الدراسة بتاريخ 26 يونيو 2026 بناءً على تحليل شامل للكود المصدري لمشروع Siro.*
|
||||
@@ -1,6 +1,5 @@
|
||||
import '../env/env.dart';
|
||||
import '../main.dart';
|
||||
import 'box_name.dart';
|
||||
|
||||
class AppLink {
|
||||
static String seferPaymentServer =
|
||||
|
||||
@@ -4,8 +4,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
|
||||
import '../../constant/colors.dart';
|
||||
import '../../constant/links.dart';
|
||||
import '../../views/widgets/snackbar.dart';
|
||||
import '../functions/crud.dart';
|
||||
|
||||
class CaptainAdminController extends GetxController {
|
||||
@@ -60,8 +60,7 @@ class CaptainAdminController extends GetxController {
|
||||
captainData = d;
|
||||
} else {
|
||||
captainData = {};
|
||||
Get.snackbar('Error', 'No captain found with this phone number',
|
||||
backgroundColor: AppColor.redColor);
|
||||
mySnackbarError('No captain found with this phone number');
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
@@ -102,17 +101,16 @@ class CaptainAdminController extends GetxController {
|
||||
if (didAuthenticate) {
|
||||
// User authenticated successfully, proceed with payment
|
||||
await addCaptainPrizeToWallet();
|
||||
Get.snackbar('Prize Added', '', backgroundColor: AppColor.greenColor);
|
||||
mySnackbarSuccess('Prize Added');
|
||||
} else {
|
||||
// Authentication failed, handle accordingly
|
||||
Get.snackbar('Authentication failed', '',
|
||||
backgroundColor: AppColor.redColor);
|
||||
mySnackbarError('Authentication failed');
|
||||
// 'Authentication failed');
|
||||
}
|
||||
} else {
|
||||
// Local authentication not available, proceed with payment without authentication
|
||||
await addCaptainPrizeToWallet();
|
||||
Get.snackbar('Prize Added', '', backgroundColor: AppColor.greenColor);
|
||||
mySnackbarSuccess('Prize Added');
|
||||
}
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:convert';
|
||||
import 'package:get/get.dart';
|
||||
import '../../constant/links.dart';
|
||||
import '../../views/widgets/snackbar.dart';
|
||||
import '../functions/crud.dart';
|
||||
|
||||
class ComplaintController extends GetxController {
|
||||
@@ -41,7 +42,7 @@ class ComplaintController extends GetxController {
|
||||
complaintList.clear();
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar("خطأ", "فشل جلب الشكاوى: $e");
|
||||
mySnackbarError("فشل جلب الشكاوى: $e");
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
@@ -61,7 +62,7 @@ class ComplaintController extends GetxController {
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
Get.snackbar("خطأ", "فشل تحديث الشكوى: $e");
|
||||
mySnackbarError("فشل تحديث الشكوى: $e");
|
||||
return false;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:convert';
|
||||
import 'package:get/get.dart';
|
||||
import '../../constant/links.dart';
|
||||
import '../../views/widgets/snackbar.dart';
|
||||
import '../functions/crud.dart';
|
||||
|
||||
class DriverDocsController extends GetxController {
|
||||
@@ -49,7 +50,7 @@ class DriverDocsController extends GetxController {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar("خطأ", "فشل جلب السائقين: $e");
|
||||
mySnackbarError("فشل جلب السائقين: $e");
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
isMoreLoading.value = false;
|
||||
@@ -70,7 +71,7 @@ class DriverDocsController extends GetxController {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar("خطأ", "فشل جلب تفاصيل السائق: $e");
|
||||
mySnackbarError("فشل جلب تفاصيل السائق: $e");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -88,7 +89,7 @@ class DriverDocsController extends GetxController {
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
Get.snackbar("خطأ", "فشل اعتماد السائق: $e");
|
||||
mySnackbarError("فشل اعتماد السائق: $e");
|
||||
return false;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
|
||||
@@ -1,34 +1,51 @@
|
||||
import 'dart:convert';
|
||||
import 'package:get/get.dart';
|
||||
import '../../constant/links.dart';
|
||||
import '../../views/widgets/snackbar.dart';
|
||||
import '../functions/crud.dart';
|
||||
|
||||
class KazanController extends GetxController {
|
||||
var kazanData = {}.obs;
|
||||
var isLoading = false.obs;
|
||||
var selectedCountry = 'Syria'.obs;
|
||||
final CRUD _crud = CRUD();
|
||||
|
||||
final List<Map<String, String>> countries = [
|
||||
{'code': 'syria', 'name': 'سوريا', 'flag': '🇸🇾'},
|
||||
{'code': 'jordan', 'name': 'الأردن', 'flag': '🇯🇴'},
|
||||
{'code': 'egypt', 'name': 'مصر', 'flag': '🇪🇬'},
|
||||
];
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
getKazan();
|
||||
}
|
||||
|
||||
void setCountry(String countryName) {
|
||||
selectedCountry.value = countryName;
|
||||
getKazan();
|
||||
}
|
||||
|
||||
Future<void> getKazan() async {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
var response = await _crud.get(link: "${AppLink.getKazanPercent}?country=syria");
|
||||
final countryParam = selectedCountry.value.toLowerCase();
|
||||
var response = await _crud.get(link: "${AppLink.getKazanPercent}?country=$countryParam");
|
||||
if (response != null && response != 'failure' && response != 'token_expired') {
|
||||
var decoded = response is String ? jsonDecode(response) : response;
|
||||
if (decoded['status'] == "success") {
|
||||
var message = decoded['message'];
|
||||
if (message is List && message.isNotEmpty) {
|
||||
kazanData.value = message[0];
|
||||
kazanData.value = Map<String, dynamic>.from(message[0]);
|
||||
kazanData['country'] = selectedCountry.value;
|
||||
} else {
|
||||
kazanData.value = {'country': selectedCountry.value};
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar("خطأ", "فشل جلب بيانات التسعير: $e");
|
||||
mySnackbarError('فشل جلب بيانات التسعير: $e');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
@@ -37,6 +54,7 @@ class KazanController extends GetxController {
|
||||
Future<bool> updateKazan(Map<String, dynamic> data) async {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
data['country'] = selectedCountry.value;
|
||||
final String link = data.containsKey('id') ? AppLink.updateKazanPercent : AppLink.addKazanPercent;
|
||||
|
||||
Map<String, String> payload = {};
|
||||
@@ -51,7 +69,7 @@ class KazanController extends GetxController {
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
Get.snackbar("خطأ", "فشل تحديث التسعير: $e");
|
||||
mySnackbarError('فشل تحديث التسعير: $e');
|
||||
return false;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../../constant/links.dart';
|
||||
import '../../views/widgets/snackbar.dart';
|
||||
import '../functions/crud.dart';
|
||||
|
||||
class MarketingController extends GetxController {
|
||||
@@ -38,22 +39,14 @@ class MarketingController extends GetxController {
|
||||
void toggleAutopilot(bool value) {
|
||||
isAutopilotEnabled = value;
|
||||
update();
|
||||
Get.snackbar(
|
||||
"Autopilot Updated".tr,
|
||||
value ? "Full Autopilot mode enabled".tr : "Approval Mode enabled".tr,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
mySnackbarInfo(value ? "Full Autopilot mode enabled".tr : "Approval Mode enabled".tr);
|
||||
}
|
||||
|
||||
// --- System Prompt Configuration Saver ---
|
||||
void savePrompt(String newPrompt) {
|
||||
systemPrompt = newPrompt;
|
||||
update();
|
||||
Get.snackbar(
|
||||
"Configuration Saved".tr,
|
||||
"Gemini system instructions updated successfully".tr,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
mySnackbarSuccess("Gemini system instructions updated successfully".tr);
|
||||
}
|
||||
|
||||
Future<void> fetchAnomalies() async {
|
||||
@@ -72,10 +65,10 @@ class MarketingController extends GetxController {
|
||||
if (res is Map && res['status'] == 'success') {
|
||||
priceAnomalies = res['message'] ?? [];
|
||||
} else {
|
||||
Get.snackbar("Error", "Failed to fetch price anomalies");
|
||||
mySnackbarError("Failed to fetch price anomalies");
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar("Error", "Network error while loading anomalies");
|
||||
mySnackbarError("Network error while loading anomalies");
|
||||
} finally {
|
||||
isLoading = false;
|
||||
update();
|
||||
@@ -123,10 +116,10 @@ class MarketingController extends GetxController {
|
||||
if (res is Map && res['status'] == 'success') {
|
||||
campaignsLog = res['message'] ?? [];
|
||||
} else {
|
||||
Get.snackbar("Error", "Failed to fetch campaign logs");
|
||||
mySnackbarError("Failed to fetch campaign logs");
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar("Error", "Network error while loading campaign logs");
|
||||
mySnackbarError("Network error while loading campaign logs");
|
||||
} finally {
|
||||
isLoading = false;
|
||||
update();
|
||||
@@ -175,17 +168,17 @@ class MarketingController extends GetxController {
|
||||
);
|
||||
if (res is Map) {
|
||||
if (res['status'] == 'success') {
|
||||
Get.snackbar("Success", "AI campaign triggered successfully! Promos sent.");
|
||||
mySnackbarSuccess("AI campaign triggered successfully! Promos sent.");
|
||||
fetchCampaignsLog();
|
||||
fetchTelemetry();
|
||||
} else {
|
||||
Get.snackbar("Campaign Alert", res['message'] ?? "Campaign rate limited.");
|
||||
mySnackbarWarning(res['message'] ?? "Campaign rate limited.");
|
||||
}
|
||||
} else {
|
||||
Get.snackbar("Error", "Failed to trigger AI campaign");
|
||||
mySnackbarError("Failed to trigger AI campaign");
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar("Error", "Network error while triggering campaign");
|
||||
mySnackbarError("Network error while triggering campaign");
|
||||
} finally {
|
||||
isLoading = false;
|
||||
update();
|
||||
@@ -224,10 +217,10 @@ class MarketingController extends GetxController {
|
||||
simulatorRecommendationMessage = data['recommendation_message'];
|
||||
}
|
||||
} else {
|
||||
Get.snackbar("Simulation Error", res['message'] ?? "Failed to run simulation");
|
||||
mySnackbarError(res['message'] ?? "Failed to run simulation");
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar("Error", "Network error during simulation");
|
||||
mySnackbarError("Network error during simulation");
|
||||
} finally {
|
||||
isLoading = false;
|
||||
update();
|
||||
@@ -340,9 +333,4 @@ class MarketingController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
// Initially fetch these too if needed, but fetch them specifically when country changes
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
|
||||
import '../../constant/colors.dart';
|
||||
import '../../constant/links.dart';
|
||||
import '../../views/widgets/snackbar.dart';
|
||||
import '../functions/crud.dart';
|
||||
|
||||
class PassengerAdminController extends GetxController {
|
||||
@@ -90,9 +90,7 @@ class PassengerAdminController extends GetxController {
|
||||
final ok = (d['status'] == 'success');
|
||||
if (ok) {
|
||||
// (اختياري) حدّث الكاش/الواجهة — مثلاً أعد الجلب
|
||||
Get.snackbar('Update successful',
|
||||
d['message']?.toString() ?? 'Passenger updated successfully',
|
||||
backgroundColor: AppColor.greenColor);
|
||||
mySnackbarSuccess(d['message']?.toString() ?? 'Passenger updated successfully');
|
||||
// await getPassengerCount(); // أو حدّث passengersData محليًا إذا متاح
|
||||
} else {
|
||||
// (اختياري) أظهر رسالة خطأ
|
||||
@@ -120,17 +118,16 @@ class PassengerAdminController extends GetxController {
|
||||
if (didAuthenticate) {
|
||||
// User authenticated successfully, proceed with payment
|
||||
await addPassengerPrizeToWallet();
|
||||
Get.snackbar('Prize Added', '', backgroundColor: AppColor.greenColor);
|
||||
mySnackbarSuccess('Prize Added');
|
||||
} else {
|
||||
// Authentication failed, handle accordingly
|
||||
Get.snackbar('Authentication failed', '',
|
||||
backgroundColor: AppColor.redColor);
|
||||
mySnackbarError('Authentication failed');
|
||||
// 'Authentication failed');
|
||||
}
|
||||
} else {
|
||||
// Local authentication not available, proceed with payment without authentication
|
||||
await addPassengerPrizeToWallet();
|
||||
Get.snackbar('Prize Added', '', backgroundColor: AppColor.greenColor);
|
||||
mySnackbarSuccess('Prize Added');
|
||||
}
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:convert';
|
||||
import 'package:get/get.dart';
|
||||
import '../../constant/links.dart';
|
||||
import '../../views/widgets/snackbar.dart';
|
||||
import '../functions/crud.dart';
|
||||
|
||||
class PromoController extends GetxController {
|
||||
@@ -27,7 +28,7 @@ class PromoController extends GetxController {
|
||||
promoList.clear();
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar("خطأ", "فشل جلب أكواد الخصم: $e");
|
||||
mySnackbarError("فشل جلب أكواد الخصم: $e");
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
@@ -43,7 +44,7 @@ class PromoController extends GetxController {
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
Get.snackbar("خطأ", "فشل إضافة كود الخصم: $e");
|
||||
mySnackbarError("فشل إضافة كود الخصم: $e");
|
||||
return false;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
@@ -59,7 +60,7 @@ class PromoController extends GetxController {
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
Get.snackbar("خطأ", "فشل حذف كود الخصم: $e");
|
||||
mySnackbarError("فشل حذف كود الخصم: $e");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -74,7 +75,7 @@ class PromoController extends GetxController {
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
Get.snackbar("خطأ", "فشل تحديث كود الخصم: $e");
|
||||
mySnackbarError("فشل تحديث كود الخصم: $e");
|
||||
return false;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../constant/links.dart';
|
||||
import '../../views/widgets/snackbar.dart';
|
||||
import '../functions/crud.dart';
|
||||
|
||||
class QualityController extends GetxController {
|
||||
@@ -21,10 +21,10 @@ class QualityController extends GetxController {
|
||||
driversBlacklist = res['message']['drivers'] ?? [];
|
||||
passengersBlacklist = res['message']['passengers'] ?? [];
|
||||
} else {
|
||||
Get.snackbar("Error", "Failed to fetch blacklist");
|
||||
mySnackbarError("Failed to fetch blacklist");
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar("Error", "Network error");
|
||||
mySnackbarError("Network error");
|
||||
} finally {
|
||||
isLoading = false;
|
||||
update();
|
||||
@@ -41,13 +41,13 @@ class QualityController extends GetxController {
|
||||
},
|
||||
);
|
||||
if (res is Map && res['status'] == 'success') {
|
||||
Get.snackbar("Success", "Driver unblocked successfully");
|
||||
mySnackbarSuccess("Driver unblocked successfully");
|
||||
fetchBlacklist(); // Refresh
|
||||
} else {
|
||||
Get.snackbar("Error", res['message'] ?? "Failed to unblock driver");
|
||||
mySnackbarError(res['message'] ?? "Failed to unblock driver");
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar("Error", "Network error");
|
||||
mySnackbarError("Network error");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,13 +61,13 @@ class QualityController extends GetxController {
|
||||
},
|
||||
);
|
||||
if (res is Map && res['status'] == 'success') {
|
||||
Get.snackbar("Success", "Passenger unblocked successfully");
|
||||
mySnackbarSuccess("Passenger unblocked successfully");
|
||||
fetchBlacklist(); // Refresh
|
||||
} else {
|
||||
Get.snackbar("Error", res['message'] ?? "Failed to unblock passenger");
|
||||
mySnackbarError(res['message'] ?? "Failed to unblock passenger");
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar("Error", "Network error");
|
||||
mySnackbarError("Network error");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,11 +82,11 @@ class QualityController extends GetxController {
|
||||
if (res is Map && res['status'] == 'success') {
|
||||
scorecardData = res['message'];
|
||||
} else {
|
||||
Get.snackbar("Error", "Failed to fetch scorecard");
|
||||
mySnackbarError("Failed to fetch scorecard");
|
||||
scorecardData = {};
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar("Error", "Network error");
|
||||
mySnackbarError("Network error");
|
||||
scorecardData = {};
|
||||
} finally {
|
||||
isLoading = false;
|
||||
@@ -94,9 +94,4 @@ class QualityController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
// fetchBlacklist() can be called when opening the page
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:ffi';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../constant/api_key.dart';
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../constant/colors.dart';
|
||||
import '../../constant/info.dart';
|
||||
import '../../constant/links.dart';
|
||||
import '../../constant/style.dart';
|
||||
import '../../main.dart';
|
||||
import '../../views/widgets/snackbar.dart';
|
||||
import '../functions/crud.dart';
|
||||
|
||||
class RegisterCaptainController extends GetxController {
|
||||
@@ -102,7 +101,7 @@ class RegisterCaptainController extends GetxController {
|
||||
driverNotCompleteRegistration = d;
|
||||
update();
|
||||
} else {
|
||||
Get.snackbar(res, '');
|
||||
mySnackbarError(res);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,9 +278,7 @@ class RegisterCaptainController extends GetxController {
|
||||
await addRegistrationCarEgypt();
|
||||
|
||||
if (isCarSaved && isDriverSaved) {
|
||||
Get.snackbar('added', '',
|
||||
backgroundColor:
|
||||
AppColor.greenColor); // Get.offAll(() => HomeCaptain());
|
||||
mySnackbarSuccess('added'); // Get.offAll(() => HomeCaptain());
|
||||
// Get.offAll(() => HomeCaptain());
|
||||
}
|
||||
}
|
||||
@@ -348,11 +345,9 @@ class RegisterCaptainController extends GetxController {
|
||||
// Handle response
|
||||
if (status1['status'] == 'success') {
|
||||
isDriverSaved = true;
|
||||
Get.snackbar('Success', 'Driver data saved successfully',
|
||||
backgroundColor: AppColor.greenColor);
|
||||
mySnackbarSuccess('Driver data saved successfully');
|
||||
} else {
|
||||
Get.snackbar('Error', 'Failed to save driver data',
|
||||
backgroundColor: Colors.red);
|
||||
mySnackbarError('Failed to save driver data');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,7 +358,7 @@ class RegisterCaptainController extends GetxController {
|
||||
"InspectionResult": responseCriminalRecordEgypt['InspectionResult'],
|
||||
});
|
||||
if (res != 'failure') {
|
||||
Get.snackbar('uploaded sucssefuly'.tr, '');
|
||||
mySnackbarSuccess('uploaded sucssefuly'.tr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,8 +389,7 @@ class RegisterCaptainController extends GetxController {
|
||||
var status = jsonDecode(res);
|
||||
if (status['status'] == 'success') {
|
||||
isCarSaved = true;
|
||||
Get.snackbar('Success', 'message',
|
||||
backgroundColor: AppColor.greenColor);
|
||||
mySnackbarSuccess('message');
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
@@ -557,7 +551,6 @@ class RegisterCaptainController extends GetxController {
|
||||
var responseData = jsonDecode(response.body);
|
||||
// Process the responseData as needed
|
||||
|
||||
var result = responseData['candidates'][0]['content']['parts'][0]['text'];
|
||||
RegExp regex = RegExp(r"```json([^`]*)```");
|
||||
String? jsonString =
|
||||
regex.firstMatch(responseData.toString())?.group(1)?.trim();
|
||||
@@ -580,8 +573,7 @@ class RegisterCaptainController extends GetxController {
|
||||
|
||||
update();
|
||||
} else {
|
||||
Get.snackbar('Error', "JSON string not found",
|
||||
backgroundColor: AppColor.redColor);
|
||||
mySnackbarError("JSON string not found");
|
||||
}
|
||||
|
||||
// Rest of your code...
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_admin/constant/links.dart';
|
||||
import 'package:siro_admin/controller/functions/crud.dart';
|
||||
import 'package:siro_admin/views/widgets/snackbar.dart';
|
||||
import '../../print.dart';
|
||||
|
||||
class SecurityV2Controller extends GetxController {
|
||||
@@ -26,9 +26,7 @@ class SecurityV2Controller extends GetxController {
|
||||
|
||||
if (res == 'failure' || res == 'token_expired') {
|
||||
Log.print('CRUD returned: $res');
|
||||
Get.snackbar("خطأ بالاتصال", "السيرفر أرجع: $res",
|
||||
backgroundColor: const Color(0x88FF0000),
|
||||
colorText: const Color(0xFFFFFFFF));
|
||||
mySnackbarError("السيرفر أرجع: $res");
|
||||
auditLogs = [];
|
||||
} else if (res != null) {
|
||||
var d = res is String ? jsonDecode(res) : res;
|
||||
@@ -44,16 +42,12 @@ class SecurityV2Controller extends GetxController {
|
||||
}
|
||||
} else {
|
||||
Log.print('Status not success: ${d['status']}');
|
||||
Get.snackbar("خطأ من السيرفر", "${d['message'] ?? d['status']}",
|
||||
backgroundColor: const Color(0x88FF0000),
|
||||
colorText: const Color(0xFFFFFFFF));
|
||||
mySnackbarError("${d['message'] ?? d['status']}");
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print('Error fetching audit logs: $e');
|
||||
Get.snackbar("خطأ برمجي", "$e",
|
||||
backgroundColor: const Color(0x88FF0000),
|
||||
colorText: const Color(0xFFFFFFFF));
|
||||
mySnackbarError("$e");
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
|
||||
@@ -2,7 +2,6 @@ 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 {
|
||||
@@ -53,10 +52,10 @@ class StaffController extends GetxController {
|
||||
_clearFields();
|
||||
Get.back();
|
||||
} else {
|
||||
mySnackeBarError('فشل في إضافة الموظف. يرجى المحاولة لاحقاً');
|
||||
mySnackbarError('فشل في إضافة الموظف. يرجى المحاولة لاحقاً');
|
||||
}
|
||||
} catch (e) {
|
||||
mySnackeBarError('حدث خطأ: $e');
|
||||
mySnackbarError('حدث خطأ: $e');
|
||||
} finally {
|
||||
isLoading = false;
|
||||
update();
|
||||
|
||||
@@ -303,11 +303,12 @@ class StaticController extends GetxController {
|
||||
totalMonthlyPassengers = data[0]['totalMonthly'].toString();
|
||||
}
|
||||
final spots = _generateSpots(data, 'day', 'totalPassengers', start, end);
|
||||
if (isCompare)
|
||||
if (isCompare) {
|
||||
chartDataPassengersCompare = spots;
|
||||
else
|
||||
} else {
|
||||
chartDataPassengers = spots;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Rides ────────────────────────────────────────────────
|
||||
Future<void> fetchRides({bool isCompare = false}) async {
|
||||
@@ -322,11 +323,12 @@ class StaticController extends GetxController {
|
||||
totalMonthlyRides = data[0]['totalMonthly'].toString();
|
||||
}
|
||||
final spots = _generateSpots(data, 'day', 'totalRides', start, end);
|
||||
if (isCompare)
|
||||
if (isCompare) {
|
||||
chartDataRidesCompare = spots;
|
||||
else
|
||||
} else {
|
||||
chartDataRides = spots;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Drivers ──────────────────────────────────────────────
|
||||
Future<void> fetchDrivers({bool isCompare = false}) async {
|
||||
|
||||
@@ -7,7 +7,6 @@ import '../../constant/api_key.dart';
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../constant/links.dart';
|
||||
import '../../main.dart';
|
||||
import '../../print.dart';
|
||||
import '../functions/crud.dart';
|
||||
|
||||
class WalletAdminController extends GetxController {
|
||||
@@ -46,7 +45,7 @@ class WalletAdminController extends GetxController {
|
||||
driversWalletPoints[i]['email'].toString(),
|
||||
);
|
||||
await Future.delayed(const Duration(seconds: 3));
|
||||
} on FormatException catch (e) {
|
||||
} on FormatException {
|
||||
// Handle the error or rethrow the exception as needed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'otp_helper.dart';
|
||||
|
||||
class OtpVerificationAdmin extends StatefulWidget {
|
||||
final String phone;
|
||||
const OtpVerificationAdmin({required this.phone});
|
||||
const OtpVerificationAdmin({super.key, required this.phone});
|
||||
|
||||
@override
|
||||
State<OtpVerificationAdmin> createState() => _OtpVerificationAdminState();
|
||||
|
||||
@@ -37,12 +37,12 @@ class OtpHelper extends GetxController {
|
||||
mySnackbarSuccess('تم إرسال رمز التحقق إلى رقمك عبر WhatsApp');
|
||||
return true;
|
||||
} else {
|
||||
mySnackeBarError('حدث خطأ من الخادم. حاول مجددًا.');
|
||||
mySnackbarError('حدث خطأ من الخادم. حاول مجددًا.');
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print('OTP SEND ERROR: $e');
|
||||
mySnackeBarError('حدث خطأ أثناء الإرسال: $e');
|
||||
mySnackbarError('حدث خطأ أثناء الإرسال: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -68,14 +68,14 @@ class OtpHelper extends GetxController {
|
||||
mySnackbarSuccess('تم التحقق من الرقم بنجاح');
|
||||
await checkAdminLogin();
|
||||
} else {
|
||||
mySnackeBarError(response['message'] ?? 'فشل في التحقق.');
|
||||
mySnackbarError(response['message'] ?? 'فشل في التحقق.');
|
||||
}
|
||||
} else {
|
||||
mySnackeBarError('فشل من الخادم. حاول مرة أخرى.');
|
||||
mySnackbarError('فشل من الخادم. حاول مرة أخرى.');
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print('OTP VERIFY ERROR: $e');
|
||||
mySnackeBarError('خطأ في التحقق: $e');
|
||||
mySnackbarError('خطأ في التحقق: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ class OtpHelper extends GetxController {
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print('LOGIN ERROR: $e');
|
||||
mySnackeBarError('حدث خطأ أثناء تسجيل الدخول: $e');
|
||||
mySnackbarError('حدث خطأ أثناء تسجيل الدخول: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -141,7 +141,7 @@ class OtpHelper extends GetxController {
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print('OTP VERIFY LOGIN ERROR: $e');
|
||||
mySnackeBarError('خطأ في التحقق من الرمز: $e');
|
||||
mySnackbarError('خطأ في التحقق من الرمز: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,9 +161,10 @@ class OtpHelper extends GetxController {
|
||||
await box.write('admin_role', role);
|
||||
Log.print('Admin role saved: $role');
|
||||
}
|
||||
if (data['phone'] != null)
|
||||
if (data['phone'] != null) {
|
||||
await box.write(BoxName.adminPhone, data['phone']);
|
||||
}
|
||||
}
|
||||
|
||||
await box.write(BoxName.phoneVerified, true);
|
||||
await box.write('admin_password', password);
|
||||
@@ -197,7 +198,7 @@ class OtpHelper extends GetxController {
|
||||
Get.back();
|
||||
verifyLoginOtp(phone, otpCode, password, fingerprint);
|
||||
} else {
|
||||
mySnackeBarError('الرجاء إدخال رمز صحيح');
|
||||
mySnackbarError('الرجاء إدخال رمز صحيح');
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -2,7 +2,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_admin/constant/links.dart';
|
||||
import 'package:siro_admin/controller/functions/crud.dart';
|
||||
import 'package:siro_admin/main.dart'; // للوصول لـ box
|
||||
import 'package:siro_admin/main.dart';
|
||||
import 'package:siro_admin/views/widgets/snackbar.dart';
|
||||
|
||||
class AdminRegisterController extends GetxController {
|
||||
final nameCtrl = TextEditingController();
|
||||
@@ -32,20 +33,14 @@ class AdminRegisterController extends GetxController {
|
||||
|
||||
if (response != 'failure') {
|
||||
if (response['status'] == 'pending') {
|
||||
Get.snackbar('نجاح', response['message'] ?? 'تم تقديم الطلب بنجاح',
|
||||
backgroundColor: Colors.green.withOpacity(0.8),
|
||||
colorText: Colors.white);
|
||||
mySnackbarSuccess(response['message'] ?? 'تم تقديم الطلب بنجاح');
|
||||
Future.delayed(const Duration(seconds: 2), () => Get.back());
|
||||
} else {
|
||||
Get.snackbar('خطأ', 'حدث خطأ غير متوقع',
|
||||
backgroundColor: Colors.red.withOpacity(0.8),
|
||||
colorText: Colors.white);
|
||||
mySnackbarError('حدث خطأ غير متوقع');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar('خطأ', 'فشل في الاتصال بالخادم',
|
||||
backgroundColor: Colors.red.withOpacity(0.8),
|
||||
colorText: Colors.white);
|
||||
mySnackbarError('فشل في الاتصال بالخادم');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import 'package:http/http.dart' as http;
|
||||
import '../../../constant/links.dart';
|
||||
import '../../constant/api_key.dart';
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../constant/colors.dart';
|
||||
import '../../main.dart';
|
||||
import '../../views/widgets/snackbar.dart';
|
||||
import '../functions/crud.dart';
|
||||
|
||||
class PaymobPayout extends GetxController {
|
||||
@@ -65,13 +65,10 @@ class PaymobPayout extends GetxController {
|
||||
'passengerID': 'admin',
|
||||
'driverID': box.read(BoxName.driverID).toString(),
|
||||
});
|
||||
Get.snackbar('Transaction successful'.tr,
|
||||
'${'Transaction successful'.tr} ${dec['amount']}',
|
||||
backgroundColor: AppColor.greenColor);
|
||||
mySnackbarSuccess('${'Transaction successful'.tr} ${dec['amount']}');
|
||||
// Get.find<CaptainWalletController>().getCaptainWalletFromRide();
|
||||
} else if (dec['disbursement_status'] == 'failed') {
|
||||
Get.snackbar('Transaction failed'.tr, 'Transaction failed'.tr,
|
||||
backgroundColor: AppColor.redColor);
|
||||
mySnackbarError('Transaction failed'.tr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +87,7 @@ class PaymobPayout extends GetxController {
|
||||
"bank_code": bankCode, //"CIB",
|
||||
"bank_transaction_type": "cash_transfer"
|
||||
};
|
||||
var res = await http
|
||||
await http
|
||||
.post(
|
||||
Uri.parse('https://payouts.paymobsolutions.com/api/secure/disburse/'),
|
||||
headers: headers,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../constant/links.dart';
|
||||
import '../../views/widgets/snackbar.dart';
|
||||
import '../functions/crud.dart';
|
||||
|
||||
class DriverController extends GetxController {
|
||||
@@ -17,7 +18,7 @@ class DriverController extends GetxController {
|
||||
drivers = (res)['message'];
|
||||
update(['drivers']); // تحديث الـ UI
|
||||
} else {
|
||||
Get.snackbar('Error', 'Failed to load drivers');
|
||||
mySnackbarError('Failed to load drivers');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +32,7 @@ class DriverController extends GetxController {
|
||||
driverDetails = (res)['message'];
|
||||
update(['driverDetails']); // تحديث صفحة التفاصيل
|
||||
} else {
|
||||
Get.snackbar('Error', 'Failed to load driver details');
|
||||
mySnackbarError('Failed to load driver details');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import 'dart:convert';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../constant/colors.dart';
|
||||
import '../../constant/links.dart';
|
||||
import '../../views/widgets/snackbar.dart';
|
||||
import '../functions/crud.dart';
|
||||
|
||||
class Driverthebest extends GetxController {
|
||||
@@ -15,7 +15,7 @@ class Driverthebest extends GetxController {
|
||||
driver = jsonDecode(res)['message'];
|
||||
update();
|
||||
} else {
|
||||
Get.snackbar('error', '', backgroundColor: AppColor.redColor);
|
||||
mySnackbarError('error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ class DriverTheBestGizaController extends GetxController {
|
||||
driver = jsonDecode(res)['message'];
|
||||
update();
|
||||
} else {
|
||||
Get.snackbar('error', '', backgroundColor: AppColor.redColor);
|
||||
mySnackbarError('error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ class DriverTheBestAlexandriaController extends GetxController {
|
||||
driver = jsonDecode(res)['message'];
|
||||
update();
|
||||
} else {
|
||||
Get.snackbar('error', '', backgroundColor: AppColor.redColor);
|
||||
mySnackbarError('error');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ import 'dart:math';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_admin/constant/colors.dart';
|
||||
import 'package:siro_admin/constant/links.dart';
|
||||
import 'package:siro_admin/controller/functions/crud.dart';
|
||||
import 'package:siro_admin/views/widgets/snackbar.dart';
|
||||
|
||||
class EmployeeController extends GetxController {
|
||||
List employee = [];
|
||||
@@ -19,7 +19,7 @@ class EmployeeController extends GetxController {
|
||||
fetchEmployee() async {
|
||||
var res = await CRUD().get(link: AppLink.getEmployee, payload: {});
|
||||
if (res is String && (res == 'failure' || res == 'token_expired')) {
|
||||
Get.snackbar('error', 'Failed to load employees', backgroundColor: AppColor.redColor);
|
||||
mySnackbarError('Failed to load employees');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ class EmployeeController extends GetxController {
|
||||
employee = jsonData['message'];
|
||||
update();
|
||||
} catch (e) {
|
||||
Get.snackbar('error', 'Invalid server response', backgroundColor: AppColor.redColor);
|
||||
mySnackbarError('Invalid server response');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,8 +59,7 @@ class EmployeeController extends GetxController {
|
||||
// You can handle the success case here, such as showing a success message
|
||||
|
||||
Get.back();
|
||||
Get.snackbar('Success', 'Employee added successfully',
|
||||
backgroundColor: AppColor.greenColor);
|
||||
mySnackbarSuccess('Employee added successfully');
|
||||
name.clear();
|
||||
education.clear();
|
||||
site.clear();
|
||||
@@ -69,8 +68,7 @@ class EmployeeController extends GetxController {
|
||||
fetchEmployee();
|
||||
} else {
|
||||
// Handle the error case by showing a snackbar with an error message
|
||||
Get.snackbar('Error', 'Failed to add employee',
|
||||
backgroundColor: AppColor.redColor);
|
||||
mySnackbarError('Failed to add employee');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,22 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:secure_string_operations/secure_string_operations.dart';
|
||||
import 'package:siro_admin/constant/info.dart';
|
||||
import 'package:siro_admin/env/env.dart';
|
||||
|
||||
import '../../constant/api_key.dart';
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../constant/char_map.dart';
|
||||
import '../../constant/colors.dart';
|
||||
import '../../constant/links.dart';
|
||||
import '../../constant/style.dart';
|
||||
import '../../main.dart';
|
||||
import '../../print.dart';
|
||||
import '../../views/widgets/elevated_btn.dart';
|
||||
import '../functions/encrypt_decrypt.dart';
|
||||
import '../../views/widgets/snackbar.dart';
|
||||
import '../notification_controller.dart';
|
||||
import 'local_notification.dart';
|
||||
import 'notification_service.dart';
|
||||
import 'token_access.dart';
|
||||
|
||||
class FirebaseMessagesController extends GetxController {
|
||||
final fcmToken = FirebaseMessaging.instance;
|
||||
@@ -53,7 +48,7 @@ class FirebaseMessagesController extends GetxController {
|
||||
await messaging.requestPermission();
|
||||
} else if (Platform.isIOS) {
|
||||
// Request permission for iOS
|
||||
NotificationSettings settings = await messaging.requestPermission(
|
||||
await messaging.requestPermission(
|
||||
alert: true,
|
||||
announcement: true,
|
||||
badge: true,
|
||||
@@ -107,7 +102,7 @@ class FirebaseMessagesController extends GetxController {
|
||||
);
|
||||
|
||||
var jsonResponse = jsonDecode(res.body);
|
||||
Log.print('jsonResponse: ${jsonResponse}');
|
||||
Log.print('jsonResponse: $jsonResponse');
|
||||
if (jsonResponse['status'] == 'success') {
|
||||
// var newData = jsonResponse['data'] as List;
|
||||
// Log.print('newData: ${newData}');
|
||||
@@ -185,7 +180,7 @@ class FirebaseMessagesController extends GetxController {
|
||||
// }
|
||||
|
||||
isSendingNotifications = false;
|
||||
Get.snackbar("Success", "All notifications sent!");
|
||||
mySnackbarSuccess("All notifications sent!");
|
||||
}
|
||||
|
||||
bool isSendingNotificationsPassenger = false;
|
||||
@@ -204,7 +199,7 @@ class FirebaseMessagesController extends GetxController {
|
||||
// }
|
||||
|
||||
isSendingNotificationsPassenger = false;
|
||||
Get.snackbar("Success", "All notifications sent!");
|
||||
mySnackbarSuccess("All notifications sent!");
|
||||
}
|
||||
|
||||
Future getAllTokenPassengers() async {
|
||||
@@ -241,9 +236,7 @@ class FirebaseMessagesController extends GetxController {
|
||||
|
||||
FirebaseMessaging.onMessage.listen((RemoteMessage 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.notification != null) {
|
||||
if (message.data.isNotEmpty && message.notification != null) {
|
||||
fireBaseTitles(message);
|
||||
}
|
||||
@@ -267,26 +260,8 @@ class FirebaseMessagesController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
SnackbarController driverAppliedTripSnakBar() {
|
||||
return Get.snackbar(
|
||||
'Driver Applied the Ride for You'.tr,
|
||||
'',
|
||||
colorText: AppColor.greenColor,
|
||||
duration: const Duration(seconds: 3),
|
||||
snackPosition: SnackPosition.TOP,
|
||||
titleText: Text(
|
||||
'Applied'.tr,
|
||||
style: const TextStyle(color: AppColor.redColor),
|
||||
),
|
||||
messageText: Text(
|
||||
'Driver Applied the Ride for You'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
icon: const Icon(Icons.approval),
|
||||
shouldIconPulse: true,
|
||||
margin: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
);
|
||||
void driverAppliedTripSnakBar() {
|
||||
mySnackbarSuccess('Driver Applied the Ride for You'.tr);
|
||||
}
|
||||
|
||||
Future<dynamic> passengerDialog(String message) {
|
||||
|
||||
@@ -14,9 +14,9 @@ import '../../constant/links.dart';
|
||||
import '../../env/env.dart';
|
||||
import '../../main.dart';
|
||||
import '../../print.dart';
|
||||
import '../../views/widgets/snackbar.dart';
|
||||
import 'device_info.dart';
|
||||
import 'encrypt_decrypt.dart';
|
||||
import 'security_checks.dart';
|
||||
import 'ssl_pinning.dart';
|
||||
|
||||
class CRUD {
|
||||
@@ -36,7 +36,7 @@ class CRUD {
|
||||
'password': AK.passnpassenger,
|
||||
'aud': '${AK.allowed}$dev',
|
||||
};
|
||||
Log.print('payload: ${payload}');
|
||||
Log.print('payload: $payload');
|
||||
var response1 = await _client.post(
|
||||
Uri.parse(AppLink.loginJwtDriver),
|
||||
body: payload,
|
||||
@@ -46,7 +46,7 @@ class CRUD {
|
||||
final decodedResponse1 = jsonDecode(response1.body);
|
||||
|
||||
final jwt = decodedResponse1['jwt'];
|
||||
Log.print('jwt: ${jwt}');
|
||||
Log.print('jwt: $jwt');
|
||||
await box.write(BoxName.jwt, X.c(X.c(X.c(jwt, cn), cC), cs));
|
||||
await storage.write(key: BoxName.jwt, value: X.c(X.c(X.c(jwt, cn), cC), cs));
|
||||
// await AppInitializer().getKey();
|
||||
@@ -334,11 +334,11 @@ class CRUD {
|
||||
Future<dynamic> postWallet(
|
||||
{required String link, Map<String, dynamic>? payload}) async {
|
||||
var s = await getJwtWallet();
|
||||
Log.print('jwt: ${s}');
|
||||
Log.print('jwt: $s');
|
||||
final hmac = box.read(BoxName.hmac);
|
||||
Log.print('hmac: ${hmac}');
|
||||
Log.print('hmac: $hmac');
|
||||
var url = Uri.parse(link);
|
||||
Log.print('url: ${url}');
|
||||
Log.print('url: $url');
|
||||
|
||||
// إضافة الـ HMAC للـ payload لزيادة التوافقية
|
||||
if (payload != null && hmac != null) {
|
||||
@@ -389,9 +389,9 @@ class CRUD {
|
||||
link: AppLink.send_whatsapp_message,
|
||||
payload: {'receiver': to, 'message': message});
|
||||
if (res != 'failure') {
|
||||
Get.snackbar('Success', 'Message sent successfully');
|
||||
mySnackbarSuccess('Message sent successfully');
|
||||
} else {
|
||||
Get.snackbar('Error', 'Failed to send message');
|
||||
mySnackbarError('Failed to send message');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,7 +399,6 @@ class CRUD {
|
||||
required String channelName,
|
||||
required String uid,
|
||||
}) async {
|
||||
var uid = box.read(BoxName.phone) ?? box.read(BoxName.phoneDriver);
|
||||
var res = await _client.get(
|
||||
Uri.parse(
|
||||
'https://repulsive-pig-rugby-shirt.cyclic.app/token?channelName=$channelName'),
|
||||
@@ -450,15 +449,6 @@ class CRUD {
|
||||
}
|
||||
|
||||
Future allMethodForAI(String prompt, driverID, imagePath) async {
|
||||
// await ImageController().choosImage(linkPHP, imagePath);
|
||||
Future.delayed(const Duration(seconds: 2));
|
||||
var extractedString = await arabicTextExtractByVisionAndAI(
|
||||
imagePath: imagePath, driverID: driverID);
|
||||
var json = jsonDecode(extractedString);
|
||||
var textValues = getAllTextValuesWithLineNumbers(json);
|
||||
// List<String> textValues = getAllTextValues(json);
|
||||
|
||||
// await AI().geminiAiExtraction(prompt, textValues);
|
||||
}
|
||||
|
||||
Map<String, List<Map<String, String>>> getAllTextValuesWithLineNumbers(
|
||||
@@ -616,7 +606,7 @@ class CRUD {
|
||||
"receiver": phone
|
||||
});
|
||||
|
||||
var res = await _client.post(
|
||||
await _client.post(
|
||||
Uri.parse(AppLink.sendSms),
|
||||
body: body,
|
||||
headers: headers,
|
||||
|
||||
@@ -12,7 +12,7 @@ import '../../print.dart';
|
||||
class DeviceHelper {
|
||||
static Future<String> getDeviceFingerprint() async {
|
||||
final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();
|
||||
var deviceData;
|
||||
Map<String, dynamic> deviceData;
|
||||
|
||||
try {
|
||||
if (Platform.isAndroid) {
|
||||
@@ -58,9 +58,6 @@ class SecurityHelper {
|
||||
bool isNotTrust = false;
|
||||
bool isJailBroken = false;
|
||||
bool isRealDevice = true;
|
||||
bool isOnExternalStorage = false;
|
||||
bool checkForIssues = false;
|
||||
bool isDevMode = false;
|
||||
bool isTampered = false;
|
||||
String bundleId = "";
|
||||
|
||||
@@ -68,15 +65,6 @@ class SecurityHelper {
|
||||
isNotTrust = await JailbreakRootDetection.instance.isNotTrust;
|
||||
isJailBroken = await JailbreakRootDetection.instance.isJailBroken;
|
||||
isRealDevice = await JailbreakRootDetection.instance.isRealDevice;
|
||||
isOnExternalStorage =
|
||||
await JailbreakRootDetection.instance.isOnExternalStorage;
|
||||
|
||||
List<JailbreakIssue> issues =
|
||||
await JailbreakRootDetection.instance.checkForIssues;
|
||||
checkForIssues = issues.isNotEmpty;
|
||||
|
||||
isDevMode = await JailbreakRootDetection.instance.isDevMode;
|
||||
|
||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||
bundleId = packageInfo.packageName;
|
||||
if (bundleId.isNotEmpty) {
|
||||
|
||||
@@ -26,8 +26,6 @@ class DigitObscuringFormatter extends TextInputFormatter {
|
||||
TextSelection updateCursorPosition(
|
||||
String maskedText, TextSelection currentSelection) {
|
||||
final cursorPosition = currentSelection.baseOffset;
|
||||
final cursorOffset =
|
||||
currentSelection.extentOffset - currentSelection.baseOffset;
|
||||
final totalDigits = maskedText.length;
|
||||
const visibleDigits = 4;
|
||||
final hiddenDigits = totalDigits - visibleDigits * 2;
|
||||
|
||||
@@ -4,8 +4,6 @@ import 'package:secure_string_operations/secure_string_operations.dart';
|
||||
|
||||
import '../../constant/char_map.dart';
|
||||
import '../../env/env.dart';
|
||||
import '../../main.dart';
|
||||
import '../../print.dart';
|
||||
|
||||
class EncryptionHelper {
|
||||
static EncryptionHelper? _instance;
|
||||
|
||||
@@ -10,6 +10,7 @@ import '../../constant/style.dart';
|
||||
import '../../main.dart';
|
||||
import '../../views/widgets/elevated_btn.dart';
|
||||
import '../../views/widgets/my_textField.dart';
|
||||
import '../../views/widgets/snackbar.dart';
|
||||
import 'crud.dart';
|
||||
|
||||
class LogOutController extends GetxController {
|
||||
@@ -20,12 +21,11 @@ class LogOutController extends GetxController {
|
||||
|
||||
Future deleteMyAccountDriver(String id) async {
|
||||
await CRUD().post(link: AppLink.removeUser, payload: {'id': id}).then(
|
||||
(value) => Get.snackbar('Deleted'.tr, 'Your Account is Deleted',
|
||||
backgroundColor: AppColor.redColor));
|
||||
(value) => mySnackbarWarning('Your Account is Deleted'.tr));
|
||||
}
|
||||
|
||||
checkBeforeDelete() async {
|
||||
var res = await CRUD().post(
|
||||
await CRUD().post(
|
||||
link: AppLink.deletecaptainAccounr,
|
||||
payload: {'id': box.read(BoxName.driverID)}).then((value) => exit(0));
|
||||
}
|
||||
@@ -88,7 +88,7 @@ class LogOutController extends GetxController {
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStateProperty.all(AppColor.redColor),
|
||||
backgroundColor: WidgetStateProperty.all(AppColor.redColor),
|
||||
),
|
||||
onPressed: () {
|
||||
// box.remove(BoxName.agreeTerms);
|
||||
@@ -131,7 +131,7 @@ class LogOutController extends GetxController {
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStateProperty.all(AppColor.redColor),
|
||||
backgroundColor: WidgetStateProperty.all(AppColor.redColor),
|
||||
),
|
||||
onPressed: () {
|
||||
// box.remove(BoxName.agreeTerms);
|
||||
@@ -172,9 +172,7 @@ class LogOutController extends GetxController {
|
||||
'email': box.read(BoxName.email),
|
||||
});
|
||||
} else {
|
||||
Get.snackbar('Email Wrong'.tr, 'Email you inserted is Wrong.'.tr,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: AppColor.redColor);
|
||||
mySnackbarError('Email you inserted is Wrong.'.tr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
@@ -5,7 +5,6 @@ import 'package:get/get.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:image_cropper/image_cropper.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
|
||||
import 'package:path_provider/path_provider.dart' as path_provider;
|
||||
@@ -16,6 +15,7 @@ import '../../constant/colors.dart';
|
||||
import '../../constant/info.dart';
|
||||
import '../../main.dart';
|
||||
import '../../print.dart';
|
||||
import '../../views/widgets/snackbar.dart';
|
||||
import 'encrypt_decrypt.dart';
|
||||
|
||||
class ImageController extends GetxController {
|
||||
@@ -141,7 +141,7 @@ class ImageController extends GetxController {
|
||||
File compressedImage = await compressImage(processedImage);
|
||||
|
||||
print('link =$link');
|
||||
Log.print('link: ${link}');
|
||||
Log.print('link: $link');
|
||||
|
||||
await uploadImage(
|
||||
compressedImage,
|
||||
@@ -153,8 +153,7 @@ class ImageController extends GetxController {
|
||||
);
|
||||
} catch (e) {
|
||||
print('Error in choosImage: $e');
|
||||
Get.snackbar('Image Upload Failed'.tr, e.toString(),
|
||||
backgroundColor: AppColor.redColor);
|
||||
mySnackbarError('Image Upload Failed'.tr);
|
||||
} finally {
|
||||
isloading = false;
|
||||
update();
|
||||
@@ -229,8 +228,7 @@ class ImageController extends GetxController {
|
||||
link,
|
||||
);
|
||||
} catch (e) {
|
||||
Get.snackbar('Image Upload Failed'.tr, e.toString(),
|
||||
backgroundColor: AppColor.redColor);
|
||||
mySnackbarError('Image Upload Failed'.tr);
|
||||
} finally {
|
||||
isloading = false;
|
||||
update();
|
||||
@@ -243,7 +241,7 @@ class ImageController extends GetxController {
|
||||
'POST',
|
||||
Uri.parse(link),
|
||||
);
|
||||
Log.print('request: ${request}');
|
||||
Log.print('request: $request');
|
||||
var length = await file.length();
|
||||
var stream = http.ByteStream(file.openRead());
|
||||
final headers = {
|
||||
@@ -251,14 +249,8 @@ class ImageController extends GetxController {
|
||||
'Bearer ${r(box.read(BoxName.jwt)).split(AppInformation.addd)[0]}',
|
||||
// 'X-HMAC-Auth': '${box.read(BoxName.hmac)}',
|
||||
};
|
||||
Log.print('headers: ${headers}');
|
||||
Log.print('headers: $headers');
|
||||
|
||||
var multipartFile = http.MultipartFile(
|
||||
'image',
|
||||
stream,
|
||||
length,
|
||||
filename: basename(file.path),
|
||||
);
|
||||
request.headers.addAll(headers);
|
||||
// Set the file name to the driverID
|
||||
|
||||
@@ -278,8 +270,7 @@ class ImageController extends GetxController {
|
||||
if (res.statusCode == 200) {
|
||||
Log.print('jsonDecode(res.body): ${jsonDecode(res.body)}');
|
||||
if (jsonDecode(res.body)['status'] == 'Image uploaded successfully!') {
|
||||
Get.snackbar('Success'.tr, 'Image uploaded successfully!'.tr,
|
||||
backgroundColor: AppColor.greenColor);
|
||||
mySnackbarSuccess('Image uploaded successfully!'.tr);
|
||||
}
|
||||
return jsonDecode(res.body);
|
||||
} else {
|
||||
@@ -330,8 +321,7 @@ class ImageController extends GetxController {
|
||||
link,
|
||||
);
|
||||
} catch (e) {
|
||||
Get.snackbar('Image Upload Failed'.tr, e.toString(),
|
||||
backgroundColor: AppColor.redColor);
|
||||
mySnackbarError('Image Upload Failed'.tr);
|
||||
} finally {
|
||||
isloading = false;
|
||||
update();
|
||||
@@ -346,12 +336,6 @@ class ImageController extends GetxController {
|
||||
|
||||
var length = await file.length();
|
||||
var stream = http.ByteStream(file.openRead());
|
||||
var multipartFile = http.MultipartFile(
|
||||
'image',
|
||||
stream,
|
||||
length,
|
||||
filename: basename(file.path),
|
||||
);
|
||||
request.headers.addAll({
|
||||
'Authorization':
|
||||
'Basic ${base64Encode(utf8.encode(AK.basicAuthCredentials.toString()))}',
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_admin/constant/colors.dart';
|
||||
|
||||
import '../../constant/links.dart';
|
||||
import '../firebase/firbase_messge.dart';
|
||||
import '../../views/widgets/snackbar.dart';
|
||||
import 'crud.dart';
|
||||
|
||||
class WalletController extends GetxController {
|
||||
@@ -35,11 +34,9 @@ class WalletController extends GetxController {
|
||||
// token, // Access token correctly
|
||||
// 'ding.wav',
|
||||
// );
|
||||
Get.snackbar('success', 'addPaymentToDriver',
|
||||
backgroundColor: AppColor.greenColor);
|
||||
mySnackbarSuccess('addPaymentToDriver');
|
||||
} else {
|
||||
Get.snackbar('error', 'addPaymentToDriver',
|
||||
backgroundColor: AppColor.redColor);
|
||||
mySnackbarError('addPaymentToDriver');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,10 +65,9 @@ class WalletController extends GetxController {
|
||||
'phone': phone,
|
||||
});
|
||||
if (res != 'failure') {
|
||||
Get.snackbar('success', 'addDrivergift300',
|
||||
backgroundColor: AppColor.greenColor);
|
||||
mySnackbarSuccess('addDrivergift300');
|
||||
} else {
|
||||
Get.snackbar('error', res, backgroundColor: AppColor.redColor);
|
||||
mySnackbarError(res);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,11 +82,9 @@ class WalletController extends GetxController {
|
||||
'driverId': driverID.toString(),
|
||||
});
|
||||
if (res != 'failure') {
|
||||
Get.snackbar('success', 'addSeferWallet',
|
||||
backgroundColor: AppColor.greenColor);
|
||||
mySnackbarSuccess('addSeferWallet');
|
||||
} else {
|
||||
Get.snackbar('error', 'addSeferWallet',
|
||||
backgroundColor: AppColor.redColor);
|
||||
mySnackbarError('addSeferWallet');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../views/widgets/snackbar.dart';
|
||||
import 'firebase/notification_service.dart';
|
||||
|
||||
class NotificationController extends GetxController {
|
||||
@@ -131,13 +132,7 @@ class NotificationController extends GetxController {
|
||||
onPressed: () async {
|
||||
if (titleController.text.trim().isEmpty ||
|
||||
bodyController.text.trim().isEmpty) {
|
||||
Get.snackbar(
|
||||
"تنبيه",
|
||||
"الرجاء تعبئة جميع الحقول",
|
||||
backgroundColor: Colors.amber.withOpacity(0.8),
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
mySnackbarWarning("الرجاء تعبئة جميع الحقول");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -171,13 +166,7 @@ class NotificationController extends GetxController {
|
||||
/// تنفيذ عملية الإرسال الفعلية
|
||||
Future<void> _processSending(String target) async {
|
||||
// إظهار تنبيه بدء العملية
|
||||
Get.snackbar(
|
||||
"جاري الإرسال",
|
||||
"يتم إرسال الإشعار لـ $target...",
|
||||
backgroundColor: _primaryAccent.withOpacity(0.2),
|
||||
colorText: Colors.white,
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
mySnackbarInfo("يتم إرسال الإشعار لـ $target...");
|
||||
|
||||
try {
|
||||
// استدعاء خدمة الإرسال
|
||||
@@ -189,19 +178,9 @@ class NotificationController extends GetxController {
|
||||
category: 'fromAdmin',
|
||||
);
|
||||
|
||||
Get.snackbar(
|
||||
"نجاح",
|
||||
"تم إرسال الإشعار بنجاح",
|
||||
backgroundColor: Colors.green.withOpacity(0.5),
|
||||
colorText: Colors.white,
|
||||
);
|
||||
mySnackbarSuccess("تم إرسال الإشعار بنجاح");
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
"خطأ",
|
||||
"فشل إرسال الإشعار: $e",
|
||||
backgroundColor: Colors.red.withOpacity(0.5),
|
||||
colorText: Colors.white,
|
||||
);
|
||||
mySnackbarError("فشل إرسال الإشعار: $e");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'dart:async';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_admin/constant/links.dart';
|
||||
|
||||
import '../../print.dart';
|
||||
|
||||
// --- Models ---
|
||||
|
||||
|
||||
@@ -78,7 +78,6 @@ class MainApp extends StatelessWidget {
|
||||
primary: AppColor.accent,
|
||||
secondary: AppColor.accent,
|
||||
surface: AppColor.surface,
|
||||
background: AppColor.bg,
|
||||
error: AppColor.danger,
|
||||
),
|
||||
dividerColor: AppColor.divider,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
@@ -10,21 +9,19 @@ import 'package:siro_admin/views/admin/quality/blacklist_page.dart';
|
||||
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../constant/colors.dart';
|
||||
import '../../constant/style.dart';
|
||||
import '../../controller/admin/dashboard_controller.dart';
|
||||
import '../../controller/admin/static_controller.dart';
|
||||
import '../../controller/functions/crud.dart';
|
||||
import '../../controller/notification_controller.dart';
|
||||
import '../../main.dart';
|
||||
import '../../print.dart';
|
||||
import '../widgets/snackbar.dart';
|
||||
import '../invoice/invoice_list_page.dart';
|
||||
import 'captain/captain.dart';
|
||||
import 'captain/syrian_driver_not_active.dart';
|
||||
import 'drivers/monitor_ride.dart';
|
||||
import 'employee/employee_page.dart';
|
||||
import 'enceypt/driver_fingerprint_migration.dart';
|
||||
import 'enceypt/encrypt.dart';
|
||||
import 'enceypt/fingerprint_migration.dart';
|
||||
import 'error/error/error_page.dart';
|
||||
import 'packages.dart';
|
||||
import 'passenger/passenger.dart';
|
||||
@@ -62,7 +59,6 @@ class _AdminHomePageState extends State<AdminHomePage>
|
||||
static const Color _surface = AppColor.surface;
|
||||
static const Color _surfaceElevated = AppColor.surfaceElevated;
|
||||
static const Color _accent = AppColor.accent;
|
||||
static const Color _accentSoft = AppColor.accentSoft;
|
||||
static const Color _accentBorder = AppColor.accentBorder;
|
||||
static const Color _danger = AppColor.danger;
|
||||
static const Color _warning = AppColor.warning;
|
||||
@@ -135,8 +131,9 @@ class _AdminHomePageState extends State<AdminHomePage>
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final category = categories[index];
|
||||
if (category.items.isEmpty)
|
||||
if (category.items.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return AnimationConfiguration.staggeredList(
|
||||
position: index,
|
||||
@@ -792,8 +789,8 @@ class _AdminHomePageState extends State<AdminHomePage>
|
||||
ActionCategory(
|
||||
title: 'المالية والإدارة',
|
||||
items: [
|
||||
ActionItem('الإدارة المالية V2', Icons.account_balance_rounded, _accent,
|
||||
() => Get.to(() => const FinancialV2Page())),
|
||||
ActionItem('الإدارة المالية V2', Icons.account_balance_rounded,
|
||||
_accent, () => Get.to(() => const FinancialV2Page())),
|
||||
ActionItem('المحفظة', Icons.account_balance_wallet_rounded, _accent,
|
||||
() => Get.to(() => Wallet())),
|
||||
ActionItem('هدية 300', Icons.card_giftcard_rounded, _warning,
|
||||
@@ -839,10 +836,10 @@ class _AdminHomePageState extends State<AdminHomePage>
|
||||
() => Get.to(() => ServerMonitorPage())),
|
||||
ActionItem('سجل الأخطاء', Icons.error_outline_rounded, _danger,
|
||||
() => Get.to(() => ErrorListPage())),
|
||||
ActionItem('encrypt fp', Icons.error_outline_rounded, _danger,
|
||||
() => Get.to(() => FingerprintMigrationTool())),
|
||||
ActionItem('encrypt fp drivers', Icons.error_outline_rounded,
|
||||
_danger, () => Get.to(() => DriverFingerprintMigrationTool())),
|
||||
// ActionItem('encrypt fp', Icons.error_outline_rounded, _danger,
|
||||
// () => Get.to(() => FingerprintMigrationTool())),
|
||||
// ActionItem('encrypt fp drivers', Icons.error_outline_rounded,
|
||||
// _danger, () => Get.to(() => DriverFingerprintMigrationTool())),
|
||||
ActionItem(
|
||||
'أداة التشفير',
|
||||
Icons.lock_rounded,
|
||||
@@ -952,7 +949,8 @@ class _AdminHomePageState extends State<AdminHomePage>
|
||||
color: const Color(0xFF4CAF50).withAlpha(30), // ~0.12 opacity
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: const Color(0xFF4CAF50).withAlpha(64)), // ~0.25 opacity
|
||||
color: const Color(0xFF4CAF50)
|
||||
.withAlpha(64)), // ~0.25 opacity
|
||||
),
|
||||
child: const Icon(Icons.message_rounded,
|
||||
color: Color(0xFF4CAF50), size: 28),
|
||||
@@ -1029,20 +1027,11 @@ class _AdminHomePageState extends State<AdminHomePage>
|
||||
Get.back();
|
||||
var driverPhones = box
|
||||
.read(BoxName.tokensDrivers)['message'] as List?;
|
||||
if (driverPhones == null || driverPhones.isEmpty)
|
||||
if (driverPhones == null || driverPhones.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
Get.snackbar(
|
||||
'بدأ الإرسال',
|
||||
'سيتم الإرسال في الخلفية',
|
||||
backgroundColor:
|
||||
const Color(0xFF4CAF50).withOpacity(0.15),
|
||||
colorText: _textPrimary,
|
||||
borderRadius: 12,
|
||||
margin: const EdgeInsets.all(16),
|
||||
icon: const Icon(Icons.check_circle_rounded,
|
||||
color: Color(0xFF4CAF50)),
|
||||
);
|
||||
mySnackbarInfo('سيتم الإرسال في الخلفية');
|
||||
|
||||
for (var driverData in driverPhones) {
|
||||
if (driverData['phone'] != null) {
|
||||
|
||||
@@ -15,12 +15,12 @@ class CaptainsPage extends StatelessWidget {
|
||||
Get.put(CaptainAdminController());
|
||||
final TextEditingController searchController = TextEditingController();
|
||||
|
||||
String myPhone = box.read(BoxName.adminPhone).toString();
|
||||
bool isSuperAdmin = false;
|
||||
final String myPhone = box.read(BoxName.adminPhone).toString();
|
||||
bool get isSuperAdmin =>
|
||||
myPhone == '963942542053' || myPhone == '963992952235';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
isSuperAdmin = myPhone == '963942542053' || myPhone == '963992952235';
|
||||
|
||||
return MyScafolld(
|
||||
title: 'Search for Captain'.tr,
|
||||
|
||||
@@ -4,11 +4,11 @@ import 'package:get/get.dart';
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../controller/admin/captain_admin_controller.dart';
|
||||
import '../../../controller/firebase/firbase_messge.dart';
|
||||
import '../../../main.dart'; // Import main to access myPhone
|
||||
import '../../widgets/elevated_btn.dart';
|
||||
import '../../widgets/my_scafold.dart';
|
||||
import '../../widgets/my_textField.dart';
|
||||
import '../../widgets/snackbar.dart';
|
||||
import '../quality/driver_scorecard_page.dart';
|
||||
import 'form_captain.dart';
|
||||
|
||||
@@ -387,8 +387,7 @@ class CaptainDetailsPage extends StatelessWidget {
|
||||
// data['passengerToken'] ?? '', // Safety check
|
||||
// 'order.wav');
|
||||
Get.back();
|
||||
Get.snackbar("Success", "Notification Sent",
|
||||
backgroundColor: Colors.green.withOpacity(0.2));
|
||||
mySnackbarSuccess("Notification Sent");
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -413,8 +412,7 @@ class CaptainDetailsPage extends StatelessWidget {
|
||||
// Call delete function here
|
||||
// controller.deleteCaptain(user['id']);
|
||||
Get.back();
|
||||
Get.snackbar("Deleted", "Captain has been removed",
|
||||
backgroundColor: Colors.red.withOpacity(0.2));
|
||||
mySnackbarWarning("Captain has been removed");
|
||||
},
|
||||
child: Text('Delete'.tr, style: const TextStyle(color: Colors.white)),
|
||||
),
|
||||
|
||||
@@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_admin/controller/functions/crud.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/admin/captain_admin_controller.dart';
|
||||
import '../../widgets/elevated_btn.dart';
|
||||
import '../../widgets/my_scafold.dart';
|
||||
import '../../widgets/my_textField.dart';
|
||||
import '../../widgets/snackbar.dart';
|
||||
|
||||
class FormCaptain extends StatefulWidget {
|
||||
const FormCaptain({super.key});
|
||||
@@ -69,7 +69,7 @@ class _FormCaptainState extends State<FormCaptain> {
|
||||
print('Updating data: $updatedData');
|
||||
|
||||
Get.back(); // Go back after saving
|
||||
Get.snackbar('Success', 'Captain data updated successfully!');
|
||||
mySnackbarSuccess('Captain data updated successfully!');
|
||||
}
|
||||
// controller.updateCaptain(updatedData);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import '../../widgets/my_scafold.dart';
|
||||
import '../../widgets/mycircular.dart';
|
||||
|
||||
class RegisterCaptain extends StatelessWidget {
|
||||
RegisterCaptain({super.key});
|
||||
const RegisterCaptain({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -7,7 +7,6 @@ import '../../../constant/info.dart';
|
||||
import '../../../constant/char_map.dart';
|
||||
|
||||
import '../../../controller/drivers/driver_not_active_controller.dart';
|
||||
import '../../../controller/functions/encrypt_decrypt.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../../print.dart';
|
||||
import 'driver_details_not_active_page.dart';
|
||||
|
||||
@@ -9,10 +9,8 @@ class DashboardV2Widget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Initialize controller
|
||||
final controller = Get.put(DashboardV2Controller());
|
||||
|
||||
return GetBuilder<DashboardV2Controller>(
|
||||
init: DashboardV2Controller(),
|
||||
builder: (ctrl) {
|
||||
if (ctrl.isLoading) {
|
||||
return const SliverToBoxAdapter(
|
||||
|
||||
@@ -12,23 +12,23 @@ class DashboardStatCard extends StatelessWidget {
|
||||
final Color? valueColor;
|
||||
|
||||
const DashboardStatCard({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.value,
|
||||
this.icon,
|
||||
this.iconColor,
|
||||
this.backgroundColor,
|
||||
this.valueColor,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Attempt to use AppStyle.boxDecoration1 properties if it's a BoxDecoration
|
||||
BoxDecoration? baseDecoration = AppStyle.boxDecoration1;
|
||||
Color? finalBackgroundColor =
|
||||
backgroundColor ?? baseDecoration?.color ?? Theme.of(context).cardColor;
|
||||
backgroundColor ?? baseDecoration.color ?? Theme.of(context).cardColor;
|
||||
BorderRadius? finalBorderRadius =
|
||||
baseDecoration?.borderRadius?.resolve(Directionality.of(context)) ??
|
||||
baseDecoration.borderRadius?.resolve(Directionality.of(context)) ??
|
||||
BorderRadius.circular(12.0);
|
||||
|
||||
return Container(
|
||||
|
||||
@@ -5,6 +5,7 @@ import '../../../constant/style.dart';
|
||||
import '../../../controller/admin/driver_docs_controller.dart';
|
||||
import '../../widgets/my_scafold.dart';
|
||||
import '../../widgets/elevated_btn.dart';
|
||||
import '../../widgets/snackbar.dart';
|
||||
import '../../../constant/links.dart';
|
||||
|
||||
class DriverDocsReviewPage extends StatelessWidget {
|
||||
@@ -100,7 +101,7 @@ class DriverDocsReviewPage extends StatelessWidget {
|
||||
const SizedBox(height: 24),
|
||||
Text('الوثائق المرفوعة', style: AppStyle.title),
|
||||
const SizedBox(height: 12),
|
||||
...docs.map((doc) => _buildDocCard(doc)).toList(),
|
||||
...docs.map((doc) => _buildDocCard(doc)),
|
||||
const SizedBox(height: 32),
|
||||
MyElevatedButton(
|
||||
title: 'اعتماد وتفعيل الحساب',
|
||||
@@ -110,7 +111,7 @@ class DriverDocsReviewPage extends StatelessWidget {
|
||||
bool success = await controller.approveDriver(id);
|
||||
if (success) {
|
||||
Get.back();
|
||||
Get.snackbar('نجاح', 'تم تفعيل حساب السائق بنجاح');
|
||||
mySnackbarSuccess('تم تفعيل حساب السائق بنجاح');
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_admin/controller/functions/crud.dart';
|
||||
import 'package:siro_admin/controller/functions/wallet.dart';
|
||||
import 'package:siro_admin/views/widgets/snackbar.dart';
|
||||
|
||||
import '../../../constant/links.dart'; // تأكد من المسار
|
||||
|
||||
@@ -17,11 +18,6 @@ class DriverGiftCheckerController extends GetxController {
|
||||
// قائمة السائقين (سنقوم بتحميلها للبحث عن الـ ID)
|
||||
List<dynamic> driversCache = [];
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
// fetchDriverCache(); // تحميل البيانات عند فتح الصفحة
|
||||
}
|
||||
|
||||
// 1. تحميل قائمة السائقين لاستخراج الـ ID منها
|
||||
Future<void> fetchDriverCache() async {
|
||||
@@ -45,8 +41,7 @@ class DriverGiftCheckerController extends GetxController {
|
||||
String phoneInput = phoneController.text.trim();
|
||||
|
||||
if (phoneInput.isEmpty) {
|
||||
Get.snackbar("تنبيه", "يرجى إدخال رقم الهاتف",
|
||||
backgroundColor: Colors.orange);
|
||||
mySnackbarWarning("يرجى إدخال رقم الهاتف");
|
||||
return;
|
||||
}
|
||||
await fetchDriverCache();
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart'; // Ensure get_storage is in pubspec.yaml
|
||||
import 'package:siro_admin/controller/functions/wallet.dart';
|
||||
import 'package:siro_admin/views/widgets/snackbar.dart';
|
||||
|
||||
// --- New Controller to handle the specific JSON URL ---
|
||||
class DriverCacheController extends GetxController {
|
||||
@@ -73,13 +74,7 @@ class DriverCacheController extends GetxController {
|
||||
paidDrivers.clear();
|
||||
box.remove('paid_drivers');
|
||||
update();
|
||||
Get.snackbar(
|
||||
"Storage Cleared",
|
||||
"Paid status history has been reset",
|
||||
backgroundColor: Colors.redAccent,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
mySnackbarInfo("Paid status history has been reset");
|
||||
}
|
||||
|
||||
// Check if driver is already paid
|
||||
@@ -93,9 +88,6 @@ class DriverTheBestRedesigned extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Put the new controller
|
||||
final controller = Get.put(DriverCacheController());
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF8FAFC), // slate-50 background
|
||||
body: SafeArea(
|
||||
@@ -580,8 +572,7 @@ class DriverTheBestRedesigned extends StatelessWidget {
|
||||
String driverId = driver['driver_id']?.toString() ?? '';
|
||||
String phone = driver['phone']?.toString() ?? '';
|
||||
if (driverId.isEmpty || driverId == 'null') {
|
||||
Get.snackbar("Error", "Cannot pay driver with missing ID",
|
||||
backgroundColor: Colors.red, colorText: Colors.white);
|
||||
mySnackbarError("Cannot pay driver with missing ID");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -642,10 +633,7 @@ class DriverTheBestRedesigned extends StatelessWidget {
|
||||
|
||||
Get.back(); // Close Dialog
|
||||
|
||||
Get.snackbar("Success", "Payment of $amount EGP sent to driver",
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM);
|
||||
mySnackbarSuccess("Payment of $amount EGP sent to driver");
|
||||
},
|
||||
textCancel: 'Cancel',
|
||||
onCancel: () => Get.back(),
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:latlong2/latlong.dart';
|
||||
import 'package:siro_admin/constant/links.dart';
|
||||
// Keep your specific imports
|
||||
import 'package:siro_admin/controller/functions/crud.dart';
|
||||
import 'package:siro_admin/views/widgets/snackbar.dart';
|
||||
|
||||
/// --------------------------------------------------------------------------
|
||||
/// 1. DATA MODELS
|
||||
@@ -49,20 +50,23 @@ String normalizePhone(String input) {
|
||||
final clean = input.replaceAll(RegExp(r'\D+'), '');
|
||||
|
||||
// Syria: 099XXXXXXX or 9639XXXXXXX
|
||||
if (clean.length == 10 && clean.startsWith('09'))
|
||||
if (clean.length == 10 && clean.startsWith('09')) {
|
||||
return '963${clean.substring(1)}';
|
||||
}
|
||||
if (clean.length == 12 && clean.startsWith('963')) return clean;
|
||||
if (clean.length == 9 && clean.startsWith('9')) return '963$clean';
|
||||
|
||||
// Jordan: 079XXXXXXX or 9627XXXXXXX
|
||||
if (clean.length == 10 && clean.startsWith('07'))
|
||||
if (clean.length == 10 && clean.startsWith('07')) {
|
||||
return '962${clean.substring(1)}';
|
||||
}
|
||||
if (clean.length == 12 && clean.startsWith('962')) return clean;
|
||||
if (clean.length == 9 && clean.startsWith('7')) return '962$clean';
|
||||
|
||||
// Egypt: 010XXXXXXXX or 2010XXXXXXXX
|
||||
if (clean.length == 11 && clean.startsWith('01'))
|
||||
if (clean.length == 11 && clean.startsWith('01')) {
|
||||
return '20${clean.substring(1)}';
|
||||
}
|
||||
if (clean.length == 13 && clean.startsWith('20')) return clean;
|
||||
|
||||
return clean;
|
||||
@@ -107,15 +111,7 @@ class RideMonitorController extends GetxController {
|
||||
|
||||
void startSearch() {
|
||||
if (phoneInputController.text.trim().isEmpty) {
|
||||
Get.snackbar(
|
||||
"تنبيه",
|
||||
"يرجى إدخال رقم الهاتف أولاً",
|
||||
backgroundColor: Colors.redAccent.withOpacity(0.9),
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
margin: const EdgeInsets.all(15),
|
||||
borderRadius: 15,
|
||||
);
|
||||
mySnackbarWarning("يرجى إدخال رقم الهاتف أولاً");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -297,9 +293,10 @@ class RideMonitorScreen extends StatelessWidget {
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(kToolbarHeight),
|
||||
child: Obx(() {
|
||||
if (controller.isTracking.value)
|
||||
if (controller.isTracking.value) {
|
||||
return const SizedBox
|
||||
.shrink(); // إخفاء الـ AppBar في وضع التتبع للخريطة الكاملة
|
||||
}
|
||||
return AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
@@ -460,7 +457,7 @@ class RideMonitorScreen extends StatelessWidget {
|
||||
PolylineLayer(
|
||||
polylines: [
|
||||
Polyline(
|
||||
points: controller.routePolyline.value,
|
||||
points: controller.routePolyline,
|
||||
strokeWidth: 6.0,
|
||||
color: primaryColor.withOpacity(0.9),
|
||||
borderStrokeWidth: 2.0,
|
||||
|
||||
@@ -1,266 +0,0 @@
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// driver_fingerprint_migration.dart
|
||||
// ───────────────────────────────────────────────────────────────
|
||||
// المنطق ببساطة:
|
||||
// 1. خذ البصمة كما هي من DB
|
||||
// 2. split('_') → احذف آخر جزء (OS version)
|
||||
// 3. join('_') → encrypt → رفع
|
||||
//
|
||||
// مثال:
|
||||
// "abc123_SamsungA51_13" → "abc123_SamsungA51" → encrypt
|
||||
// "TECNO_LH7n-GL_14" → "TECNO_LH7n-GL" → encrypt
|
||||
// "unknown_2412DPC0AG_15" → "unknown_2412DPC0AG" → encrypt
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../controller/functions/crud.dart';
|
||||
import '../../../controller/functions/encrypt_decrypt.dart';
|
||||
import '../../../print.dart';
|
||||
|
||||
class DriverFingerprintMigrationTool extends StatefulWidget {
|
||||
const DriverFingerprintMigrationTool({super.key});
|
||||
|
||||
@override
|
||||
State<DriverFingerprintMigrationTool> createState() =>
|
||||
_DriverFingerprintMigrationToolState();
|
||||
}
|
||||
|
||||
class _DriverFingerprintMigrationToolState
|
||||
extends State<DriverFingerprintMigrationTool> {
|
||||
bool _isRunning = false;
|
||||
bool _isDone = false;
|
||||
int _total = 0;
|
||||
int _processed = 0;
|
||||
int _updated = 0;
|
||||
int _failed = 0;
|
||||
String _currentLog = '';
|
||||
|
||||
static const int _batchSize = 50;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// المنطق الأساسي — حذف آخر جزء بعد "_"
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
String _removeLastSegment(String raw) {
|
||||
final parts = raw.split('_');
|
||||
if (parts.length <= 1) return raw; // جزء واحد — ما في شيء نحذفه
|
||||
parts.removeLast();
|
||||
return parts.join('_');
|
||||
}
|
||||
|
||||
Future<void> _startMigration() async {
|
||||
setState(() {
|
||||
_isRunning = true;
|
||||
_isDone = false;
|
||||
_processed = 0;
|
||||
_updated = 0;
|
||||
_failed = 0;
|
||||
_currentLog = 'جارٍ جلب بصمات السائقين...';
|
||||
});
|
||||
|
||||
try {
|
||||
final records = await _fetchAll();
|
||||
if (records == null) {
|
||||
_log('❌ فشل في جلب البيانات');
|
||||
setState(() => _isRunning = false);
|
||||
return;
|
||||
}
|
||||
|
||||
_total = records.length;
|
||||
_log('✅ تم جلب $_total بصمة — بدء المعالجة...');
|
||||
|
||||
for (int i = 0; i < records.length; i += _batchSize) {
|
||||
final batch = records.skip(i).take(_batchSize).toList();
|
||||
_log('⚙️ معالجة ${i + 1} → ${i + batch.length} من $_total');
|
||||
await Future.wait(batch.map(_processSingle));
|
||||
if (i + _batchSize < records.length) {
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
}
|
||||
}
|
||||
|
||||
_log('🎉 اكتمل!\nمحدَّث: $_updated | فاشل: $_failed');
|
||||
setState(() {
|
||||
_isDone = true;
|
||||
_isRunning = false;
|
||||
});
|
||||
} catch (e) {
|
||||
_log('❌ خطأ: $e');
|
||||
setState(() => _isRunning = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>?> _fetchAll() async {
|
||||
try {
|
||||
final response = await CRUD().post(
|
||||
link: AppLink.getAllDriverFingerprints,
|
||||
payload: {'admin_key': 'iuyweiruinakjbfkajkjlkmalkcxnlahd'},
|
||||
);
|
||||
if (response == 'failure' || response == null) return null;
|
||||
|
||||
final data = response['data'];
|
||||
if (data is! List) return null;
|
||||
|
||||
return List<Map<String, dynamic>>.from(data);
|
||||
} catch (e) {
|
||||
Log.print('fetchAll error: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _processSingle(Map<String, dynamic> record) async {
|
||||
final captainId = record['captain_id']?.toString() ?? '';
|
||||
final rawFp = record['fingerPrint']?.toString() ?? '';
|
||||
|
||||
if (captainId.isEmpty || rawFp.isEmpty) {
|
||||
setState(() {
|
||||
_failed++;
|
||||
_processed++;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// ── حذف آخر جزء (OS version) ─────────────────────────────
|
||||
final String newRaw = _removeLastSegment(rawFp);
|
||||
final String encrypted = EncryptionHelper.instance.encryptData(newRaw);
|
||||
|
||||
Log.print('🔄 [$captainId] "$rawFp" → "$newRaw" → encrypted');
|
||||
|
||||
// ── رفع للسيرفر ──────────────────────────────────────────
|
||||
final res = await CRUD().post(
|
||||
link: AppLink.updateDriverFingerprintAdmin,
|
||||
payload: {
|
||||
'captain_id': captainId,
|
||||
'fingerprint': encrypted,
|
||||
'admin_key': 'iuyweiruinakjbfkajkjlkmalkcxnlahd',
|
||||
},
|
||||
);
|
||||
|
||||
if (res != 'failure' && res?['status'] == 'success') {
|
||||
setState(() {
|
||||
_updated++;
|
||||
_processed++;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_failed++;
|
||||
_processed++;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print('❌ [$captainId]: $e');
|
||||
setState(() {
|
||||
_failed++;
|
||||
_processed++;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _log(String msg) {
|
||||
Log.print(msg);
|
||||
setState(() => _currentLog = msg);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Driver FP Migration')),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.orange.shade200),
|
||||
),
|
||||
child: const Text(
|
||||
'⚠️ تُستخدم مرة واحدة فقط\n\n'
|
||||
'"abc123_Samsung_13" → "abc123_Samsung" → encrypt\n'
|
||||
'"TECNO_LH7n_14" → "TECNO_LH7n" → encrypt',
|
||||
style:
|
||||
TextStyle(fontSize: 13, height: 1.7, fontFamily: 'monospace'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (_total > 0) ...[
|
||||
Text('التقدم: $_processed / $_total',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
LinearProgressIndicator(
|
||||
value: _total > 0 ? _processed / _total : 0,
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
color: _isDone ? Colors.green : Colors.blue,
|
||||
minHeight: 8,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
if (_processed > 0)
|
||||
Row(children: [
|
||||
_chip('محدَّث', _updated, Colors.green),
|
||||
const SizedBox(width: 8),
|
||||
_chip('فاشل', _failed, Colors.red),
|
||||
]),
|
||||
const SizedBox(height: 16),
|
||||
if (_currentLog.isNotEmpty)
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(_currentLog,
|
||||
style:
|
||||
const TextStyle(fontFamily: 'monospace', fontSize: 12)),
|
||||
),
|
||||
const Spacer(),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 52,
|
||||
child: ElevatedButton(
|
||||
onPressed: (_isRunning || _isDone) ? null : _startMigration,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: _isDone ? Colors.green : Colors.blue,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
child: _isRunning
|
||||
? const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white, strokeWidth: 2)),
|
||||
SizedBox(width: 12),
|
||||
Text('جارٍ الترحيل...',
|
||||
style:
|
||||
TextStyle(color: Colors.white, fontSize: 16)),
|
||||
],
|
||||
)
|
||||
: Text(
|
||||
_isDone ? '✅ اكتمل الترحيل' : 'بدء ترحيل بصمات السائقين',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _chip(String label, int value, Color color) => Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: color.withOpacity(0.3)),
|
||||
),
|
||||
child: Text('$label: $value',
|
||||
style: TextStyle(color: color, fontWeight: FontWeight.bold)),
|
||||
);
|
||||
}
|
||||
@@ -9,14 +9,11 @@ import '../../../constant/box_name.dart';
|
||||
// ─── Custom Colors ────────────────────────────────────────────────────────────
|
||||
class _AppColors {
|
||||
static const bg = Color(0xFF0A0D14);
|
||||
static const surface = Color(0xFF111622);
|
||||
static const card = Color(0xFF161D2E);
|
||||
static const border = Color(0xFF1F2D4A);
|
||||
static const accent = Color(0xFF00E5FF);
|
||||
static const accentDim = Color(0xFF0097A7);
|
||||
static const accentGlow = Color(0x2200E5FF);
|
||||
static const accentDecrypt = Color(0xFF7C4DFF);
|
||||
static const accentDecryptDim = Color(0xFF512DA8);
|
||||
static const accentDecryptGlow = Color(0x227C4DFF);
|
||||
static const textPrimary = Color(0xFFE8F0FE);
|
||||
static const textSec = Color(0xFF7A8BAA);
|
||||
@@ -27,7 +24,7 @@ class _AppColors {
|
||||
class EncryptToolPage extends StatefulWidget {
|
||||
final String adminToken;
|
||||
|
||||
const EncryptToolPage({Key? key, required this.adminToken}) : super(key: key);
|
||||
const EncryptToolPage({super.key, required this.adminToken});
|
||||
|
||||
@override
|
||||
State<EncryptToolPage> createState() => _EncryptToolPageState();
|
||||
|
||||
@@ -1,366 +0,0 @@
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// fingerprint_migration.dart
|
||||
// ───────────────────────────────────────────────────────────────
|
||||
// أداة ترحيل البصمات القديمة للنظام الجديد
|
||||
// ───────────────────────────────────────────────────────────────
|
||||
// المشكلة:
|
||||
// البصمة القديمة = encrypt(androidId_model_osVersion)
|
||||
// البصمة الجديدة = encrypt(androidId_model)
|
||||
//
|
||||
// الحل:
|
||||
// 1. نجيب كل البصمات من السيرفر (batch 50 في المرة)
|
||||
// 2. نفك تشفير كل بصمة بـ EncryptionHelper
|
||||
// 3. نحذف آخر جزء (osVersion) مع الـ _ قبله
|
||||
// 4. نعيد التشفير
|
||||
// 5. نرفع البصمة المحدّثة للسيرفر
|
||||
//
|
||||
// يُستخدم مرة واحدة فقط ثم يُحذف من التطبيق
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:siro_admin/controller/functions/encrypt_decrypt.dart' as X;
|
||||
|
||||
import '../../../constant/char_map.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../controller/functions/crud.dart';
|
||||
import '../../../controller/functions/encrypt_decrypt.dart';
|
||||
import '../../../print.dart';
|
||||
|
||||
class FingerprintMigrationTool extends StatefulWidget {
|
||||
const FingerprintMigrationTool({super.key});
|
||||
|
||||
@override
|
||||
State<FingerprintMigrationTool> createState() =>
|
||||
_FingerprintMigrationToolState();
|
||||
}
|
||||
|
||||
class _FingerprintMigrationToolState extends State<FingerprintMigrationTool> {
|
||||
// ── حالة الترحيل ──────────────────────────────────────────
|
||||
bool _isRunning = false;
|
||||
bool _isDone = false;
|
||||
int _total = 0;
|
||||
int _processed = 0;
|
||||
int _updated = 0; // بصمات تم تحديثها
|
||||
int _skipped = 0; // بصمات كانت بالفعل بالنظام الجديد
|
||||
int _failed = 0; // فشل في المعالجة
|
||||
String _currentLog = '';
|
||||
|
||||
static const int _batchSize = 50;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// الدالة الرئيسية للترحيل
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
Future<void> _startMigration() async {
|
||||
setState(() {
|
||||
_isRunning = true;
|
||||
_isDone = false;
|
||||
_processed = 0;
|
||||
_updated = 0;
|
||||
_skipped = 0;
|
||||
_failed = 0;
|
||||
_currentLog = 'جارٍ جلب البصمات من السيرفر...';
|
||||
});
|
||||
|
||||
try {
|
||||
// ── 1. جلب كل البصمات من السيرفر ──────────────────────
|
||||
final allFingerprints = await _fetchAllFingerprints();
|
||||
|
||||
if (allFingerprints == null) {
|
||||
_log('❌ فشل في جلب البيانات من السيرفر');
|
||||
setState(() => _isRunning = false);
|
||||
return;
|
||||
}
|
||||
|
||||
_total = allFingerprints.length;
|
||||
_log('✅ تم جلب $_total بصمة — بدء المعالجة...');
|
||||
|
||||
// ── 2. معالجة على batches ──────────────────────────────
|
||||
for (int i = 0; i < allFingerprints.length; i += _batchSize) {
|
||||
final batch = allFingerprints.skip(i).take(_batchSize).toList();
|
||||
|
||||
_log('⚙️ معالجة ${i + 1} → ${i + batch.length} من $_total');
|
||||
|
||||
// معالجة الـ batch بالتوازي
|
||||
await Future.wait(
|
||||
batch.map((record) => _processSingleRecord(record)),
|
||||
);
|
||||
|
||||
// استراحة قصيرة بين الـ batches لحماية السيرفر
|
||||
if (i + _batchSize < allFingerprints.length) {
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
}
|
||||
}
|
||||
|
||||
_log('🎉 اكتمل الترحيل!\n'
|
||||
'محدَّث: $_updated | متجاوز: $_skipped | فاشل: $_failed');
|
||||
|
||||
setState(() {
|
||||
_isDone = true;
|
||||
_isRunning = false;
|
||||
});
|
||||
} catch (e) {
|
||||
_log('❌ خطأ عام: $e');
|
||||
setState(() => _isRunning = false);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// جلب كل البصمات من السيرفر
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
Future<List<Map<String, dynamic>>?> _fetchAllFingerprints() async {
|
||||
try {
|
||||
final response = await CRUD().post(
|
||||
link: AppLink.getAllFingerprints, // أضفه في AppLink
|
||||
payload: {
|
||||
'admin_key': 'iuyweiruinakjbfkajkjlkmalkcxnlahd'
|
||||
}, // مفتاح أمان للـ endpoint
|
||||
);
|
||||
|
||||
if (response == 'failure' || response == null) return null;
|
||||
|
||||
final data = response['data'];
|
||||
if (data is! List) return null;
|
||||
|
||||
return List<Map<String, dynamic>>.from(data);
|
||||
} catch (e) {
|
||||
Log.print('fetchAllFingerprints error: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// معالجة بصمة واحدة
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
Future<void> _processSingleRecord(Map<String, dynamic> record) async {
|
||||
final String passengerID = record['passengerID']?.toString() ?? '';
|
||||
final String encryptedFp = record['fingerPrint']?.toString() ?? '';
|
||||
final String userType = record['userType']?.toString() ?? 'passenger';
|
||||
|
||||
if (passengerID.isEmpty || encryptedFp.isEmpty) {
|
||||
setState(() {
|
||||
_failed++;
|
||||
_processed++;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// ── فك التشفير ────────────────────────────────────────
|
||||
final String rawFp = EncryptionHelper.instance.decryptData(encryptedFp);
|
||||
|
||||
// ── تحليل البصمة ──────────────────────────────────────
|
||||
// الشكل القديم: "androidId_model_osVersion" (3 أجزاء أو أكثر)
|
||||
// الشكل الجديد: "androidId_model" (جزءان فقط)
|
||||
final List<String> parts = rawFp.split('_');
|
||||
|
||||
if (parts.length <= 2) {
|
||||
// البصمة بالفعل بالنظام الجديد — تجاوزها
|
||||
setState(() {
|
||||
_skipped++;
|
||||
_processed++;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// ── حذف آخر جزء (osVersion) ──────────────────────────
|
||||
// مثال: "abc123_SamsungA51_13" → "abc123_SamsungA51"
|
||||
// نأخذ أول جزأين فقط بغض النظر عن عدد الأجزاء
|
||||
final String newRawFp = '${parts[0]}_${parts[1]}';
|
||||
|
||||
// ── إعادة التشفير ─────────────────────────────────────
|
||||
final String newEncryptedFp =
|
||||
EncryptionHelper.instance.encryptData(newRawFp);
|
||||
|
||||
// ── رفع البصمة الجديدة للسيرفر ───────────────────────
|
||||
final response = await CRUD().post(
|
||||
link: AppLink.updateFingerprintAdmin, // أضفه في AppLink
|
||||
payload: {
|
||||
'passengerID': passengerID,
|
||||
'fingerprint': newEncryptedFp,
|
||||
'userType': userType,
|
||||
'admin_key': 'iuyweiruinakjbfkajkjlkmalkcxnlahd',
|
||||
},
|
||||
);
|
||||
|
||||
if (response != 'failure' && response?['status'] == 'success') {
|
||||
setState(() {
|
||||
_updated++;
|
||||
_processed++;
|
||||
});
|
||||
Log.print('✅ Updated: $passengerID | $rawFp → $newRawFp');
|
||||
} else {
|
||||
setState(() {
|
||||
_failed++;
|
||||
_processed++;
|
||||
});
|
||||
Log.print('❌ Failed update: $passengerID');
|
||||
}
|
||||
} catch (e) {
|
||||
// فشل فك التشفير أو إعادة التشفير
|
||||
setState(() {
|
||||
_failed++;
|
||||
_processed++;
|
||||
});
|
||||
Log.print('❌ Process error for $passengerID: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _log(String message) {
|
||||
Log.print(message);
|
||||
setState(() => _currentLog = message);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// UI
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Fingerprint Migration Tool')),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// ── شرح الأداة ──────────────────────────────────
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.orange.shade200),
|
||||
),
|
||||
child: const Text(
|
||||
'⚠️ هذه الأداة تُستخدم مرة واحدة فقط\n'
|
||||
'تقوم بتحديث بصمات الأجهزة القديمة\n'
|
||||
'لتكون متوافقة مع النظام الجديد (بدون OS version)',
|
||||
style: TextStyle(fontSize: 14, height: 1.6),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
print(EncryptionHelper.instance.decryptData('hbgbitbXrXrBr'));
|
||||
},
|
||||
child: Text(
|
||||
"Decrypt Test",
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
print(EncryptionHelper.instance.encryptData(
|
||||
'1B501143-C579-461C-B556-4E8B390EEFE1_iPhone'));
|
||||
},
|
||||
child: Text(
|
||||
"Encrypt Test",
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
print(r('hbgbitbXrXrBr'));
|
||||
},
|
||||
child: Text(
|
||||
"decrypt X.r",
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// ── شريط التقدم ─────────────────────────────────
|
||||
if (_total > 0) ...[
|
||||
Text('التقدم: $_processed / $_total'),
|
||||
const SizedBox(height: 8),
|
||||
LinearProgressIndicator(
|
||||
value: _total > 0 ? _processed / _total : 0,
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
color: _isDone ? Colors.green : Colors.blue,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
// ── إحصائيات ────────────────────────────────────
|
||||
if (_processed > 0)
|
||||
Row(children: [
|
||||
_statChip('محدَّث', _updated, Colors.green),
|
||||
const SizedBox(width: 8),
|
||||
_statChip('متجاوز', _skipped, Colors.blue),
|
||||
const SizedBox(width: 8),
|
||||
_statChip('فاشل', _failed, Colors.red),
|
||||
]),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// ── السجل الحالي ─────────────────────────────────
|
||||
if (_currentLog.isNotEmpty)
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(_currentLog,
|
||||
style: const TextStyle(fontFamily: 'monospace')),
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
|
||||
// ── زر التشغيل ──────────────────────────────────
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 50,
|
||||
child: ElevatedButton(
|
||||
onPressed: (_isRunning || _isDone) ? null : _startMigration,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: _isDone ? Colors.green : Colors.blue,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
child: _isRunning
|
||||
? const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white, strokeWidth: 2),
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
Text('جارٍ الترحيل...',
|
||||
style: TextStyle(color: Colors.white)),
|
||||
],
|
||||
)
|
||||
: Text(
|
||||
_isDone ? '✅ اكتمل الترحيل' : 'بدء الترحيل',
|
||||
style:
|
||||
const TextStyle(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _statChip(String label, int value, Color color) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: color.withOpacity(0.3)),
|
||||
),
|
||||
child: Text('$label: $value',
|
||||
style: TextStyle(color: color, fontWeight: FontWeight.bold)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:siro_admin/constant/colors.dart';
|
||||
import 'package:siro_admin/constant/links.dart';
|
||||
import 'package:siro_admin/controller/functions/crud.dart';
|
||||
|
||||
@@ -40,7 +39,7 @@ class ErrorLog {
|
||||
}
|
||||
|
||||
class ErrorListPage extends StatefulWidget {
|
||||
const ErrorListPage({Key? key}) : super(key: key);
|
||||
const ErrorListPage({super.key});
|
||||
|
||||
@override
|
||||
State<ErrorListPage> createState() => _ErrorListPageState();
|
||||
|
||||
@@ -204,8 +204,9 @@ class FinancialV2Page extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildSettlementsList(List<dynamic> settlements) {
|
||||
if (settlements.isEmpty)
|
||||
if (settlements.isEmpty) {
|
||||
return const Center(child: Text('لا توجد تسويات معلقة'));
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
|
||||
@@ -214,7 +214,7 @@ class MarketingPage extends StatelessWidget {
|
||||
const SizedBox(width: 24),
|
||||
_buildStatItem(
|
||||
'الفارق',
|
||||
'${((double.tryParse(anomaly['competitor_price']?.toString() ?? '0') ?? 0) - (double.tryParse(anomaly['our_price']?.toString() ?? '0') ?? 0)).toStringAsFixed(2)}',
|
||||
((double.tryParse(anomaly['competitor_price']?.toString() ?? '0') ?? 0) - (double.tryParse(anomaly['our_price']?.toString() ?? '0') ?? 0)).toStringAsFixed(2),
|
||||
_info,
|
||||
),
|
||||
],
|
||||
@@ -264,7 +264,7 @@ class MarketingPage extends StatelessWidget {
|
||||
: 0.0;
|
||||
|
||||
List<FlSpot> competitorSpots = [];
|
||||
if (hourlyData is List && hourlyData.isNotEmpty) {
|
||||
if (hourlyData.isNotEmpty) {
|
||||
for (int i = 0; i < hourlyData.length && i < 24; i++) {
|
||||
final val = hourlyData[i];
|
||||
final double avg = double.tryParse((val['avg_price_per_km'] ?? 0).toString()) ?? 0.0;
|
||||
@@ -280,7 +280,7 @@ class MarketingPage extends StatelessWidget {
|
||||
|
||||
List<FlSpot> siroSpots = [];
|
||||
if (siroSpeedPrice > 0) {
|
||||
for (int i = 0; i < (hourlyData is List ? hourlyData.length : 1); i++) {
|
||||
for (int i = 0; i < hourlyData.length; i++) {
|
||||
siroSpots.add(FlSpot(i.toDouble(), siroSpeedPrice));
|
||||
}
|
||||
}
|
||||
@@ -303,7 +303,7 @@ class MarketingPage extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
hourlyData is List && hourlyData.isNotEmpty
|
||||
hourlyData.isNotEmpty
|
||||
? 'مقارنة أسعار Siro مقابل متوسط المنافسين لآخر ${hourlyData.length} ساعة'
|
||||
: 'مقارنة أسعار Siro مقابل متوسط المنافسين لآخر 24 ساعة',
|
||||
style: const TextStyle(fontSize: 10, color: _textSecondary),
|
||||
@@ -346,7 +346,7 @@ class MarketingPage extends StatelessWidget {
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
minX: 0,
|
||||
maxX: ((hourlyData is List ? hourlyData.length : 1) - 1).toDouble(),
|
||||
maxX: (hourlyData.length - 1).toDouble(),
|
||||
minY: 0,
|
||||
maxY: chartMaxY,
|
||||
lineBarsData: [
|
||||
@@ -391,10 +391,8 @@ class MarketingPage extends StatelessWidget {
|
||||
return GetBuilder<MarketingController>(
|
||||
builder: (c) {
|
||||
final heatmapList = c.heatmapData;
|
||||
final siroSpeedPrice = c.currentSiroPriceHeatmap;
|
||||
|
||||
List<Map<String, dynamic>> regions = [];
|
||||
if (heatmapList is List && heatmapList.isNotEmpty) {
|
||||
if (heatmapList.isNotEmpty) {
|
||||
for (final item in heatmapList) {
|
||||
final double pci = double.tryParse(item['pci'].toString()) ?? 1.0;
|
||||
final pct = ((1 - pci) * 100).abs().toStringAsFixed(1);
|
||||
@@ -433,7 +431,6 @@ class MarketingPage extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
...regions.map((region) {
|
||||
final pciVal = (region['pci'] as num).toDouble();
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Column(
|
||||
@@ -462,7 +459,7 @@ class MarketingPage extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -725,7 +722,7 @@ class MarketingPage extends StatelessWidget {
|
||||
style: const TextStyle(fontSize: 10, color: _textSecondary),
|
||||
),
|
||||
value: c.isAutopilotEnabled,
|
||||
activeColor: _accent,
|
||||
activeThumbColor: _accent,
|
||||
onChanged: c.toggleAutopilot,
|
||||
),
|
||||
),
|
||||
@@ -978,65 +975,4 @@ class MarketingPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWinbackCampaignsSection(BuildContext context, MarketingController c) {
|
||||
return Card(
|
||||
color: _surface,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16), side: const BorderSide(color: _divider)),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Row(
|
||||
children: [
|
||||
Icon(Icons.radar, color: _accent, size: 24),
|
||||
SizedBox(width: 8),
|
||||
Text('حملات استعادة العملاء (Win-Back)', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: _textPrimary)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'يتم البحث عن الركاب المنقطعين عن التطبيق والذين يتواجدون حالياً بالقرب من مناطق تشهد أسعار ذروة لدى المنافسين.',
|
||||
style: TextStyle(color: _textSecondary, fontSize: 11),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => c.fetchWinbackTargets(),
|
||||
icon: const Icon(Icons.person_search, size: 18),
|
||||
label: const Text('بحث عن أهداف حالية'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: _accent.withOpacity(0.1),
|
||||
foregroundColor: _accent,
|
||||
elevation: 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (c.winbackTotalCount > 0) ...[
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
// TODO: Implement trigger specific winback campaign
|
||||
Get.snackbar("Campaign Triggered", "SMS sent to ${c.winbackTotalCount} targets");
|
||||
},
|
||||
icon: const Icon(Icons.send),
|
||||
label: Text('إرسال SMS لـ ${c.winbackTotalCount}'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: _success,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:get/get.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:siro_admin/constant/links.dart';
|
||||
import 'package:siro_admin/controller/functions/crud.dart';
|
||||
import 'package:siro_admin/views/widgets/my_textField.dart';
|
||||
import 'package:siro_admin/views/widgets/snackbar.dart';
|
||||
|
||||
import '../../print.dart';
|
||||
|
||||
@@ -14,7 +14,6 @@ const Color _bg = Color(0xFF0D1117);
|
||||
const Color _surface = Color(0xFF161B22);
|
||||
const Color _surfaceElevated = Color(0xFF1C2333);
|
||||
const Color _accent = Color(0xFF00D4AA);
|
||||
const Color _danger = Color(0xFFFF5370);
|
||||
const Color _warning = Color(0xFFFFCB6B);
|
||||
const Color _info = Color(0xFF82AAFF);
|
||||
const Color _textPrimary = Color(0xFFE6EDF3);
|
||||
@@ -556,15 +555,7 @@ class PackageController extends GetxController {
|
||||
|
||||
updatePackages(String id, String version) async {
|
||||
if (version.trim().isEmpty) {
|
||||
Get.snackbar(
|
||||
'تنبيه',
|
||||
'يرجى إدخال رقم الإصدار',
|
||||
backgroundColor: _warning.withOpacity(0.15),
|
||||
colorText: _textPrimary,
|
||||
borderRadius: 12,
|
||||
margin: const EdgeInsets.all(16),
|
||||
icon: const Icon(Icons.warning_rounded, color: _warning),
|
||||
);
|
||||
mySnackbarWarning('يرجى إدخال رقم الإصدار');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -578,26 +569,10 @@ class PackageController extends GetxController {
|
||||
|
||||
if (response != 'failure') {
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'تم التحديث',
|
||||
'تم تحديث الإصدار بنجاح',
|
||||
backgroundColor: _accent.withOpacity(0.15),
|
||||
colorText: _textPrimary,
|
||||
borderRadius: 12,
|
||||
margin: const EdgeInsets.all(16),
|
||||
icon: const Icon(Icons.check_circle_rounded, color: _accent),
|
||||
);
|
||||
mySnackbarSuccess('تم تحديث الإصدار بنجاح');
|
||||
fetchPackages();
|
||||
} else {
|
||||
Get.snackbar(
|
||||
'خطأ',
|
||||
'فشل التحديث، يرجى المحاولة مجدداً',
|
||||
backgroundColor: _danger.withOpacity(0.15),
|
||||
colorText: _textPrimary,
|
||||
borderRadius: 12,
|
||||
margin: const EdgeInsets.all(16),
|
||||
icon: const Icon(Icons.error_rounded, color: _danger),
|
||||
);
|
||||
mySnackbarError('فشل التحديث، يرجى المحاولة مجدداً');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import '../../widgets/elevated_btn.dart';
|
||||
import '../../widgets/my_scafold.dart';
|
||||
import '../../widgets/my_textField.dart';
|
||||
import '../../widgets/mycircular.dart';
|
||||
import '../../widgets/snackbar.dart';
|
||||
import 'passenger_details_page.dart';
|
||||
|
||||
class Passengrs extends StatelessWidget {
|
||||
@@ -376,15 +377,7 @@ class Passengrs extends StatelessWidget {
|
||||
Text('Cancel'.tr, style: const TextStyle(color: Colors.grey))),
|
||||
);
|
||||
} else {
|
||||
Get.snackbar(
|
||||
'Not Allowed'.tr,
|
||||
'Prizes can only be added on Saturdays.'.tr,
|
||||
backgroundColor: Colors.red.withOpacity(0.1),
|
||||
colorText: Colors.red,
|
||||
icon: const Icon(Icons.error_outline, color: Colors.red),
|
||||
snackPosition: SnackPosition.TOP,
|
||||
margin: const EdgeInsets.all(10),
|
||||
);
|
||||
mySnackbarWarning('Prizes can only be added on Saturdays.'.tr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,15 +3,14 @@ import 'package:get/get.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../views/widgets/snackbar.dart';
|
||||
import '../../../controller/admin/passenger_admin_controller.dart';
|
||||
import '../../../controller/functions/crud.dart';
|
||||
import '../../../controller/firebase/firbase_messge.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../main.dart'; // To access 'box' for admin phone check
|
||||
import '../../widgets/elevated_btn.dart';
|
||||
import '../../widgets/my_scafold.dart';
|
||||
import '../../widgets/my_textField.dart';
|
||||
import 'form_passenger.dart';
|
||||
|
||||
class PassengerDetailsPage extends StatelessWidget {
|
||||
const PassengerDetailsPage({super.key});
|
||||
@@ -399,8 +398,7 @@ class PassengerDetailsPage extends StatelessWidget {
|
||||
// data['passengerToken'],
|
||||
// 'order.wav');
|
||||
Get.back();
|
||||
Get.snackbar('Success', 'Notification sent successfully!',
|
||||
backgroundColor: Colors.green.withOpacity(0.2));
|
||||
mySnackbarSuccess('Notification sent successfully!');
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -438,13 +436,11 @@ class PassengerDetailsPage extends StatelessWidget {
|
||||
// 3. Handle Result
|
||||
if (res['status'] == 'success') {
|
||||
Get.back(); // Go back to list page
|
||||
Get.snackbar('Deleted', 'Passenger removed successfully',
|
||||
backgroundColor: Colors.red.withOpacity(0.2));
|
||||
mySnackbarWarning('Passenger removed successfully');
|
||||
// Ideally, trigger a refresh on the controller here
|
||||
// Get.find<PassengerAdminController>().getAll();
|
||||
} else {
|
||||
Get.snackbar('Error', res['message'] ?? 'Failed to delete',
|
||||
backgroundColor: Colors.red.withOpacity(0.2));
|
||||
mySnackbarError(res['message'] ?? 'Failed to delete');
|
||||
}
|
||||
},
|
||||
child: Text('Delete'.tr, style: const TextStyle(color: Colors.white)),
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:get/get.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/admin/kazan_controller.dart';
|
||||
import '../../../views/widgets/snackbar.dart';
|
||||
import '../../widgets/my_scafold.dart';
|
||||
import '../../widgets/elevated_btn.dart';
|
||||
|
||||
@@ -17,29 +18,26 @@ class KazanEditorPage extends StatelessWidget {
|
||||
title: 'تعديل أسعار كازان'.tr,
|
||||
isleading: true,
|
||||
body: [
|
||||
Obx(() => controller.isLoading.value && controller.kazanData.isEmpty
|
||||
Column(
|
||||
children: [
|
||||
_buildCountryDropdown(),
|
||||
Expanded(
|
||||
child: Obx(() => controller.isLoading.value && controller.kazanData.isEmpty
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// ⚙️ الإعدادات العامة
|
||||
_buildSectionHeader('⚙️ الإعدادات العامة'),
|
||||
_buildGeneralSettings(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 🚗 أسعار الكيلومتر لكل نوع سيارة
|
||||
_buildSectionHeader('🚗 أسعار الكيلومتر لكل نوع سيارة'),
|
||||
_buildKmPricesGrid(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// ⏱️ أسعار الدقيقة
|
||||
_buildSectionHeader('⏱️ أسعار الدقيقة (حسب وقت اليوم)'),
|
||||
_buildMinutePrices(),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// 💾 زر الحفظ
|
||||
MyElevatedButton(
|
||||
title: '💾 حفظ جميع التعديلات',
|
||||
icon: Icons.save_rounded,
|
||||
@@ -49,7 +47,48 @@ class KazanEditorPage extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCountryDropdown() {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||
child: Obx(() => Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.surface,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: AppColor.divider),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
value: controller.selectedCountry.value,
|
||||
isExpanded: true,
|
||||
icon: const Icon(Icons.keyboard_arrow_down_rounded),
|
||||
style: AppStyle.title.copyWith(fontSize: 16),
|
||||
items: controller.countries.map((c) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: c['name'],
|
||||
child: Row(
|
||||
children: [
|
||||
Text(c['flag']!, style: const TextStyle(fontSize: 22)),
|
||||
const SizedBox(width: 12),
|
||||
Text(c['name']!),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (val) {
|
||||
if (val != null) controller.setCountry(val);
|
||||
},
|
||||
),
|
||||
),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -373,26 +412,18 @@ class KazanEditorPage extends StatelessWidget {
|
||||
final data = Map<String, dynamic>.from(controller.kazanData);
|
||||
data['adminId'] = 'admin1';
|
||||
|
||||
// التأكد من وجود country (إذا لم يكن موجوداً، استخدم 'syria')
|
||||
// التأكد من وجود country
|
||||
if (!data.containsKey('country') ||
|
||||
data['country'] == null ||
|
||||
data['country'].toString().isEmpty) {
|
||||
data['country'] = 'Syria';
|
||||
data['country'] = controller.selectedCountry.value;
|
||||
}
|
||||
|
||||
bool success = await controller.updateKazan(data);
|
||||
if (success) {
|
||||
Get.snackbar("نجاح", "تم تحديث الأسعار بنجاح",
|
||||
backgroundColor: AppColor.successSoft,
|
||||
colorText: AppColor.textPrimary,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
margin: const EdgeInsets.all(16));
|
||||
mySnackbarSuccess('تم تحديث أسعار ${controller.selectedCountry.value} بنجاح');
|
||||
} else {
|
||||
Get.snackbar("خطأ", "فشل تحديث الأسعار",
|
||||
backgroundColor: Colors.red.shade100,
|
||||
colorText: AppColor.textPrimary,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
margin: const EdgeInsets.all(16));
|
||||
mySnackbarError('فشل تحديث الأسعار');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,7 +435,7 @@ class _RidesDashboardScreenState extends State<RidesDashboardScreen>
|
||||
const Color(0xFFEF4444))),
|
||||
Obx(() => _buildStatCard(
|
||||
'الإيرادات',
|
||||
'${controller.totalRevenue.value.toStringAsFixed(0)}',
|
||||
controller.totalRevenue.value.toStringAsFixed(0),
|
||||
Icons.payments_rounded,
|
||||
const Color(0xFFF59E0B))),
|
||||
],
|
||||
@@ -817,8 +817,9 @@ class _RidesDashboardScreenState extends State<RidesDashboardScreen>
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
String formattedPhone = phone;
|
||||
if (!formattedPhone.startsWith('+'))
|
||||
if (!formattedPhone.startsWith('+')) {
|
||||
formattedPhone = '+$formattedPhone';
|
||||
}
|
||||
final Uri launchUri = Uri(scheme: 'tel', path: formattedPhone);
|
||||
if (await canLaunchUrl(launchUri)) await launchUrl(launchUri);
|
||||
},
|
||||
@@ -860,11 +861,13 @@ class _RidesDashboardScreenState extends State<RidesDashboardScreen>
|
||||
|
||||
// Helper Methods for Status
|
||||
Color _getStatusColor(String status) {
|
||||
if (status == 'Begin' || status == 'Arrived')
|
||||
if (status == 'Begin' || status == 'Arrived') {
|
||||
return const Color(0xFF10B981);
|
||||
}
|
||||
if (status == 'Finished') return const Color(0xFF14B8A6);
|
||||
if (status.contains('Cancel') || status == 'TimeOut')
|
||||
if (status.contains('Cancel') || status == 'TimeOut') {
|
||||
return const Color(0xFFEF4444);
|
||||
}
|
||||
if (status == 'New') return const Color(0xFF3B82F6);
|
||||
return Colors.grey;
|
||||
}
|
||||
@@ -872,8 +875,9 @@ class _RidesDashboardScreenState extends State<RidesDashboardScreen>
|
||||
String _getStatusText(String status) {
|
||||
if (status == 'Begin' || status == 'Arrived') return 'جارية';
|
||||
if (status == 'Finished') return 'مكتملة';
|
||||
if (status == 'CancelFromDriver' || status == 'CancelFromDriverAfterApply')
|
||||
if (status == 'CancelFromDriver' || status == 'CancelFromDriverAfterApply') {
|
||||
return 'ألغى السائق';
|
||||
}
|
||||
if (status == 'CancelFromPassenger') return 'ألغى الراكب';
|
||||
if (status == 'TimeOut') return 'انتهى الوقت';
|
||||
if (status == 'New') return 'جديدة';
|
||||
@@ -1147,12 +1151,6 @@ class _RideMapMonitorScreenState extends State<RideMapMonitorScreen> {
|
||||
required String phone,
|
||||
required Color color,
|
||||
}) {
|
||||
String displayPhone = phone;
|
||||
if (!widget.isAdmin && phone.length > 4) {
|
||||
displayPhone =
|
||||
phone.substring(phone.length - 4).padLeft(phone.length, '*');
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
@@ -1192,8 +1190,9 @@ class _RideMapMonitorScreenState extends State<RideMapMonitorScreen> {
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
String formattedPhone = phone;
|
||||
if (!formattedPhone.startsWith('+'))
|
||||
if (!formattedPhone.startsWith('+')) {
|
||||
formattedPhone = '+$formattedPhone';
|
||||
}
|
||||
final Uri launchUri = Uri(scheme: 'tel', path: formattedPhone);
|
||||
if (await canLaunchUrl(launchUri)) await launchUrl(launchUri);
|
||||
},
|
||||
|
||||
@@ -9,7 +9,7 @@ import '../../widgets/mycircular.dart';
|
||||
|
||||
class Rides extends StatelessWidget {
|
||||
Rides({super.key});
|
||||
RideAdminController rideAdminController = Get.put(RideAdminController());
|
||||
final RideAdminController rideAdminController = Get.put(RideAdminController());
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MyScafolld(title: 'Rides'.tr, isleading: true, body: [
|
||||
|
||||
@@ -2,15 +2,12 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_admin/constant/colors.dart';
|
||||
import 'package:siro_admin/controller/admin/security_v2_controller.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class AuditLogsPage extends StatelessWidget {
|
||||
const AuditLogsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = Get.put(SecurityV2Controller());
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColor.bg,
|
||||
appBar: AppBar(
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:get/get.dart';
|
||||
import '../../../controller/server/server_monitor_controller.dart';
|
||||
|
||||
class ServerMonitorPage extends StatelessWidget {
|
||||
const ServerMonitorPage({Key? key}) : super(key: key);
|
||||
const ServerMonitorPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -187,7 +187,7 @@ class AddStaffPage extends StatelessWidget {
|
||||
border: Border.all(color: Colors.white.withOpacity(0.05)),
|
||||
),
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: value,
|
||||
initialValue: value,
|
||||
dropdownColor: fillColor,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
decoration: InputDecoration(
|
||||
|
||||
@@ -33,7 +33,7 @@ class _PendingAdminsPageState extends State<PendingAdminsPage> {
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
mySnackeBarError('فشل في جلب البيانات: $e');
|
||||
mySnackbarError('فشل في جلب البيانات: $e');
|
||||
} finally {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
@@ -53,7 +53,7 @@ class _PendingAdminsPageState extends State<PendingAdminsPage> {
|
||||
_fetchPendingAdmins(); // تحديث القائمة
|
||||
}
|
||||
} catch (e) {
|
||||
mySnackeBarError('حدث خطأ: $e');
|
||||
mySnackbarError('حدث خطأ: $e');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_admin/constant/colors.dart';
|
||||
import 'package:siro_admin/controller/admin/analytics_v2_controller.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class AdvancedAnalyticsPage extends StatelessWidget {
|
||||
const AdvancedAnalyticsPage({super.key});
|
||||
@@ -191,8 +190,9 @@ class AdvancedAnalyticsPage extends StatelessWidget {
|
||||
final passengers = data['passenger_daily'] as List<dynamic>? ?? [];
|
||||
final drivers = data['driver_daily'] as List<dynamic>? ?? [];
|
||||
|
||||
if (passengers.isEmpty && drivers.isEmpty)
|
||||
if (passengers.isEmpty && drivers.isEmpty) {
|
||||
return const Center(child: Text('لا توجد بيانات'));
|
||||
}
|
||||
|
||||
List<BarChartGroupData> barGroups = [];
|
||||
int maxLength =
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart' hide TextDirection;
|
||||
import 'package:siro_admin/controller/functions/launch.dart';
|
||||
|
||||
import '../../../controller/admin/static_controller.dart';
|
||||
|
||||
@@ -26,8 +26,6 @@ class StaticDash extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = Get.put(StaticController());
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: _bg,
|
||||
body: GetBuilder<StaticController>(
|
||||
|
||||
@@ -9,7 +9,7 @@ import '../../widgets/my_scafold.dart';
|
||||
|
||||
class Wallet extends StatelessWidget {
|
||||
Wallet({super.key});
|
||||
WalletAdminController walletAdminController =
|
||||
final WalletAdminController walletAdminController =
|
||||
Get.put(WalletAdminController());
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_admin/env/env.dart';
|
||||
|
||||
import '../../controller/auth/login_controller.dart';
|
||||
import '../../controller/auth/otp_helper.dart';
|
||||
import '../../controller/functions/crud.dart';
|
||||
import '../../print.dart';
|
||||
import '../widgets/snackbar.dart';
|
||||
import '../admin/admin_home_page.dart';
|
||||
|
||||
// ─── Colors (نفس نظام الألوان المستخدم في التطبيق) ──────────────────────────
|
||||
@@ -15,10 +12,8 @@ class _C {
|
||||
static const border = Color(0xFF1F2D4A);
|
||||
static const accent = Color(0xFF00E5FF);
|
||||
static const accentGlow = Color(0x2200E5FF);
|
||||
static const accentDim = Color(0xFF0097A7);
|
||||
static const textPrimary = Color(0xFFE8F0FE);
|
||||
static const textSec = Color(0xFF7A8BAA);
|
||||
static const error = Color(0xFFFF5252);
|
||||
static const inputBg = Color(0xFF0C1120);
|
||||
}
|
||||
|
||||
@@ -44,7 +39,7 @@ class _AdminLoginPageState extends State<AdminLoginPage>
|
||||
final phone = _phoneController.text.trim();
|
||||
|
||||
if (password.isEmpty) {
|
||||
Get.snackbar('خطأ', 'يرجى إدخال كلمة المرور');
|
||||
mySnackbarError('يرجى إدخال كلمة المرور');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -506,7 +501,7 @@ class _SubmitButtonState extends State<_SubmitButton>
|
||||
children: [
|
||||
Icon(Icons.send_rounded, color: Colors.white, size: 18),
|
||||
SizedBox(width: 10),
|
||||
const Text(
|
||||
Text(
|
||||
'تسجيل الدخول',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
|
||||
@@ -5,7 +5,6 @@ import 'package:siro_admin/controller/auth/register_controller.dart';
|
||||
class _C {
|
||||
static const bg = Color(0xFF0A0D14);
|
||||
static const card = Color(0xFF161D2E);
|
||||
static const border = Color(0xFF1F2D4A);
|
||||
static const accent = Color(0xFF00E5FF);
|
||||
static const textPrimary = Color(0xFFE8F0FE);
|
||||
static const textSec = Color(0xFF7A8BAA);
|
||||
|
||||
@@ -10,6 +10,7 @@ import '../../constant/box_name.dart';
|
||||
import '../../constant/info.dart';
|
||||
import '../../controller/functions/encrypt_decrypt.dart';
|
||||
import '../../main.dart';
|
||||
import '../../views/widgets/snackbar.dart';
|
||||
|
||||
class AddInvoicePage extends StatefulWidget {
|
||||
const AddInvoicePage({super.key});
|
||||
@@ -38,7 +39,6 @@ class _AddInvoicePageState extends State<AddInvoicePage> {
|
||||
Future<void> uploadInvoice() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
final driverID = '123'; // قيمة افتراضية أو يمكن جلبها من الكونترولر
|
||||
final invoiceNumber = generateInvoiceNumber();
|
||||
final amount = _amountController.text.trim();
|
||||
final itemName = _itemNameController.text.trim();
|
||||
@@ -56,7 +56,6 @@ class _AddInvoicePageState extends State<AddInvoicePage> {
|
||||
|
||||
final uri = Uri.parse(AppLink.addInvoice);
|
||||
final request = http.MultipartRequest('POST', uri)
|
||||
..fields['driverID'] = driverID
|
||||
..fields['invoiceNumber'] = invoiceNumber
|
||||
..fields['amount'] = amount
|
||||
..fields['name'] = itemName
|
||||
@@ -84,15 +83,7 @@ class _AddInvoicePageState extends State<AddInvoicePage> {
|
||||
}
|
||||
|
||||
if (data['status'] == 'success') {
|
||||
Get.snackbar(
|
||||
'نجاح',
|
||||
'تم حفظ الفاتورة بنجاح',
|
||||
backgroundColor: Colors.green.withOpacity(0.1),
|
||||
colorText: Colors.green[800],
|
||||
snackPosition: SnackPosition.TOP,
|
||||
margin: const EdgeInsets.all(10),
|
||||
borderRadius: 20,
|
||||
);
|
||||
mySnackbarSuccess('تم حفظ الفاتورة بنجاح');
|
||||
|
||||
_itemNameController.clear();
|
||||
_amountController.clear();
|
||||
@@ -103,20 +94,10 @@ class _AddInvoicePageState extends State<AddInvoicePage> {
|
||||
Get.back(result: true);
|
||||
});
|
||||
} else {
|
||||
Get.snackbar(
|
||||
'تنبيه',
|
||||
data['message'] ?? 'حدث خطأ غير معروف',
|
||||
backgroundColor: Colors.red.withOpacity(0.1),
|
||||
colorText: Colors.red[800],
|
||||
);
|
||||
mySnackbarWarning(data['message'] ?? 'حدث خطأ غير معروف');
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
'خطأ في الاتصال',
|
||||
e.toString(),
|
||||
backgroundColor: Colors.red.withOpacity(0.1),
|
||||
colorText: Colors.red[800],
|
||||
);
|
||||
mySnackbarError(e.toString());
|
||||
} finally {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:get/get.dart';
|
||||
import '../../constant/links.dart';
|
||||
import '../../controller/functions/crud.dart';
|
||||
import '../../print.dart';
|
||||
import '../widgets/snackbar.dart';
|
||||
import 'add_invoice_page.dart';
|
||||
|
||||
// نفترض أن هذا الموديل موجود في مشروعك، إذا لم يكن موجوداً يرجى إضافته أو تعديل الاستيراد
|
||||
@@ -55,9 +56,7 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
|
||||
} else {
|
||||
if (mounted) {
|
||||
setState(() => isLoading = false);
|
||||
Get.snackbar("تنبيه", "لا توجد فواتير لعرضها أو حدث خطأ",
|
||||
backgroundColor: Colors.orange.withOpacity(0.2),
|
||||
colorText: Colors.orange[900]);
|
||||
mySnackbarWarning("لا توجد فواتير لعرضها أو حدث خطأ");
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -325,8 +324,7 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
|
||||
if (imageUrl != null && imageUrl.isNotEmpty) {
|
||||
_showImageDialog(context, imageUrl);
|
||||
} else {
|
||||
Get.snackbar("تنبيه", "لا توجد صورة مرفقة",
|
||||
backgroundColor: Colors.grey[200], colorText: Colors.black);
|
||||
mySnackbarWarning("لا توجد صورة مرفقة");
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
@@ -407,7 +405,7 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
"$amount",
|
||||
amount,
|
||||
style: TextStyle(
|
||||
color: moneyColor,
|
||||
fontWeight: FontWeight.w900,
|
||||
|
||||
@@ -9,11 +9,11 @@ class MyCircleContainer extends StatelessWidget {
|
||||
final Color borderColor;
|
||||
|
||||
MyCircleContainer({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.child,
|
||||
this.backgroundColor = AppColor.secondaryColor,
|
||||
this.borderColor = AppColor.accentColor,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
final controller = Get.put(CircleController());
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -13,12 +12,12 @@ class MyElevatedButton extends StatelessWidget {
|
||||
final IconData? icon;
|
||||
|
||||
const MyElevatedButton({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.onPressed,
|
||||
this.kolor,
|
||||
this.icon,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -5,11 +5,11 @@ import '../../constant/style.dart';
|
||||
|
||||
class IconWidgetMenu extends StatelessWidget {
|
||||
const IconWidgetMenu({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.onpressed,
|
||||
required this.icon,
|
||||
required this.title,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
final VoidCallback onpressed;
|
||||
final IconData icon;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
|
||||
import '../../constant/colors.dart';
|
||||
import '../../constant/style.dart';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
@@ -2,122 +2,261 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../constant/colors.dart';
|
||||
enum _SnackVariant { success, error, info, warning }
|
||||
|
||||
class SnackbarConfig {
|
||||
static const duration = Duration(seconds: 3);
|
||||
static const animationDuration = Duration(milliseconds: 300);
|
||||
static const margin = EdgeInsets.symmetric(horizontal: 16, vertical: 10);
|
||||
static const borderRadius = 12.0;
|
||||
static const elevation = 6.0;
|
||||
extension _VariantProps on _SnackVariant {
|
||||
Color get baseColor => switch (this) {
|
||||
_SnackVariant.success => const Color(0xFF1A9E5C),
|
||||
_SnackVariant.error => const Color(0xFFD93025),
|
||||
_SnackVariant.info => const Color(0xFF1A73E8),
|
||||
_SnackVariant.warning => const Color(0xFFF29900),
|
||||
};
|
||||
|
||||
static final BoxShadow shadow = BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
Color get surfaceColor => switch (this) {
|
||||
_SnackVariant.success => const Color(0xFFF0FBF5),
|
||||
_SnackVariant.error => const Color(0xFFFEF2F1),
|
||||
_SnackVariant.info => const Color(0xFFF0F6FF),
|
||||
_SnackVariant.warning => const Color(0xFFFFF8E6),
|
||||
};
|
||||
|
||||
IconData get icon => switch (this) {
|
||||
_SnackVariant.success => Icons.check_circle_rounded,
|
||||
_SnackVariant.error => Icons.error_rounded,
|
||||
_SnackVariant.info => Icons.info_rounded,
|
||||
_SnackVariant.warning => Icons.warning_amber_rounded,
|
||||
};
|
||||
|
||||
String get label => switch (this) {
|
||||
_SnackVariant.success => 'Success',
|
||||
_SnackVariant.error => 'Error',
|
||||
_SnackVariant.info => 'Info',
|
||||
_SnackVariant.warning => 'Warning',
|
||||
};
|
||||
}
|
||||
|
||||
class _SnackContent extends StatefulWidget {
|
||||
final String message;
|
||||
final _SnackVariant variant;
|
||||
|
||||
const _SnackContent({required this.message, required this.variant});
|
||||
|
||||
@override
|
||||
State<_SnackContent> createState() => _SnackContentState();
|
||||
}
|
||||
|
||||
class _SnackContentState extends State<_SnackContent>
|
||||
with TickerProviderStateMixin {
|
||||
late final AnimationController _ctrl;
|
||||
late final AnimationController _scaleCtrl;
|
||||
late final Animation<double> _scaleAnim;
|
||||
late final Animation<double> _progressAnim;
|
||||
|
||||
static const Duration _displayDuration = Duration(seconds: 4);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_ctrl = AnimationController(vsync: this, duration: _displayDuration);
|
||||
_scaleCtrl = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
);
|
||||
_scaleAnim = CurvedAnimation(
|
||||
parent: _scaleCtrl,
|
||||
curve: Curves.elasticOut,
|
||||
);
|
||||
_progressAnim = Tween<double>(begin: 1.0, end: 0.0).animate(
|
||||
CurvedAnimation(parent: _ctrl, curve: Curves.linear),
|
||||
);
|
||||
_scaleCtrl.forward();
|
||||
_ctrl.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ctrl.dispose();
|
||||
_scaleCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final v = widget.variant;
|
||||
final accent = v.baseColor;
|
||||
final surface = v.surfaceColor;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: surface,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
border: Border.all(color: accent.withAlpha(46), width: 1.2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: accent.withAlpha(31),
|
||||
blurRadius: 20,
|
||||
spreadRadius: -2,
|
||||
offset: const Offset(0, 6),
|
||||
),
|
||||
BoxShadow(
|
||||
color: Colors.black.withAlpha(15),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(14, 14, 10, 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
ScaleTransition(
|
||||
scale: _scaleAnim,
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: accent.withAlpha(31),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(v.icon, color: accent, size: 22),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
v.label.tr,
|
||||
style: TextStyle(
|
||||
color: accent,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
widget.message,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF424242),
|
||||
fontSize: 13.5,
|
||||
height: 1.4,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
HapticFeedback.lightImpact();
|
||||
_closeSnackbar(context);
|
||||
},
|
||||
child: Container(
|
||||
width: 30,
|
||||
height: 30,
|
||||
margin: const EdgeInsets.only(left: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withAlpha(25),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.close_rounded,
|
||||
size: 16,
|
||||
color: Colors.grey[500],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
AnimatedBuilder(
|
||||
animation: _progressAnim,
|
||||
builder: (_, __) => Stack(
|
||||
children: [
|
||||
Container(height: 3, color: accent.withAlpha(20)),
|
||||
FractionallySizedBox(
|
||||
widthFactor: _progressAnim.value,
|
||||
child: Container(
|
||||
height: 3,
|
||||
decoration: BoxDecoration(
|
||||
color: accent.withAlpha(115),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(4),
|
||||
bottomRight: Radius.circular(4),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
SnackbarController mySnackeBarError(String message) {
|
||||
// Trigger error haptic feedback
|
||||
void _closeSnackbar(BuildContext context) {
|
||||
ScaffoldMessenger.maybeOf(context)?.hideCurrentSnackBar();
|
||||
}
|
||||
}
|
||||
|
||||
int _retryCount = 0;
|
||||
|
||||
void _show(_SnackVariant variant, String message) {
|
||||
if (Get.context == null) {
|
||||
if (_retryCount < 3) {
|
||||
_retryCount++;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _show(variant, message));
|
||||
}
|
||||
return;
|
||||
}
|
||||
_retryCount = 0;
|
||||
|
||||
final context = Get.context;
|
||||
if (context == null) return;
|
||||
|
||||
final messenger = ScaffoldMessenger.maybeOf(context);
|
||||
if (messenger == null) return;
|
||||
|
||||
messenger.clearSnackBars();
|
||||
|
||||
switch (variant) {
|
||||
case _SnackVariant.error:
|
||||
case _SnackVariant.warning:
|
||||
HapticFeedback.mediumImpact();
|
||||
|
||||
return Get.snackbar(
|
||||
'Error'.tr,
|
||||
message,
|
||||
backgroundColor: AppColor.redColor.withOpacity(0.95),
|
||||
colorText: AppColor.secondaryColor,
|
||||
icon: const Icon(
|
||||
Icons.error_outline_rounded,
|
||||
color: AppColor.secondaryColor,
|
||||
size: 28,
|
||||
),
|
||||
shouldIconPulse: true,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
margin: SnackbarConfig.margin,
|
||||
borderRadius: SnackbarConfig.borderRadius,
|
||||
duration: SnackbarConfig.duration,
|
||||
animationDuration: SnackbarConfig.animationDuration,
|
||||
forwardAnimationCurve: Curves.easeOutCirc,
|
||||
reverseAnimationCurve: Curves.easeInCirc,
|
||||
boxShadows: [SnackbarConfig.shadow],
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
titleText: Text(
|
||||
'Error'.tr,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
letterSpacing: 0.2,
|
||||
),
|
||||
),
|
||||
messageText: Text(
|
||||
message,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.95),
|
||||
fontSize: 14,
|
||||
height: 1.3,
|
||||
),
|
||||
),
|
||||
onTap: (_) {
|
||||
case _SnackVariant.success:
|
||||
HapticFeedback.lightImpact();
|
||||
Get.closeCurrentSnackbar();
|
||||
},
|
||||
isDismissible: true,
|
||||
dismissDirection: DismissDirection.horizontal,
|
||||
overlayBlur: 0.8,
|
||||
overlayColor: Colors.black12,
|
||||
case _SnackVariant.info:
|
||||
HapticFeedback.selectionClick();
|
||||
}
|
||||
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: _SnackContent(message: message, variant: variant),
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
margin: EdgeInsets.zero,
|
||||
padding: EdgeInsets.zero,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
duration: const Duration(seconds: 4),
|
||||
dismissDirection: DismissDirection.up,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
SnackbarController mySnackbarSuccess(String message) {
|
||||
// Trigger success haptic feedback
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
return Get.snackbar(
|
||||
'Success'.tr,
|
||||
message,
|
||||
backgroundColor: AppColor.greenColor.withOpacity(0.95),
|
||||
colorText: AppColor.secondaryColor,
|
||||
icon: const Icon(
|
||||
Icons.check_circle_outline_rounded,
|
||||
color: AppColor.secondaryColor,
|
||||
size: 28,
|
||||
),
|
||||
shouldIconPulse: true,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
margin: SnackbarConfig.margin,
|
||||
borderRadius: SnackbarConfig.borderRadius,
|
||||
duration: SnackbarConfig.duration,
|
||||
animationDuration: SnackbarConfig.animationDuration,
|
||||
forwardAnimationCurve: Curves.easeOutCirc,
|
||||
reverseAnimationCurve: Curves.easeInCirc,
|
||||
boxShadows: [SnackbarConfig.shadow],
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
titleText: Text(
|
||||
'Success'.tr,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
letterSpacing: 0.2,
|
||||
),
|
||||
),
|
||||
messageText: Text(
|
||||
message,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.95),
|
||||
fontSize: 14,
|
||||
height: 1.3,
|
||||
),
|
||||
),
|
||||
onTap: (_) {
|
||||
HapticFeedback.lightImpact();
|
||||
Get.closeCurrentSnackbar();
|
||||
},
|
||||
isDismissible: true,
|
||||
dismissDirection: DismissDirection.horizontal,
|
||||
overlayBlur: 0.8,
|
||||
overlayColor: Colors.black12,
|
||||
);
|
||||
}
|
||||
void mySnackbarSuccess(String message) => _show(_SnackVariant.success, message);
|
||||
void mySnackbarError(String message) => _show(_SnackVariant.error, message);
|
||||
void mySnackbarInfo(String message) => _show(_SnackVariant.info, message);
|
||||
void mySnackbarWarning(String message) => _show(_SnackVariant.warning, message);
|
||||
|
||||
@@ -617,7 +617,7 @@ packages:
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
image:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image
|
||||
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
|
||||
@@ -937,7 +937,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_provider:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||
|
||||
@@ -69,6 +69,8 @@ dependencies:
|
||||
jailbreak_root_detection: ^1.1.5
|
||||
package_info_plus: ^4.0.2
|
||||
|
||||
image: any
|
||||
path_provider: any
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
@@ -121,7 +121,7 @@ https://siromove.com/invite.php?code=$couponCode&app=rider
|
||||
// mySnackbarSuccess('Contacts sync completed successfully!'.tr);
|
||||
}
|
||||
} catch (e) {
|
||||
// mySnackeBarError('An error occurred during contact sync: $e'.tr);
|
||||
// mySnackbarError('An error occurred during contact sync: $e'.tr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ https://siromove.com/invite.php?code=$couponCode&app=rider
|
||||
Get.back(); // إغلاق شاشة التحميل
|
||||
|
||||
if (validContacts.isEmpty) {
|
||||
mySnackeBarError('No contacts with phone numbers found'.tr);
|
||||
mySnackbarError('No contacts with phone numbers found'.tr);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -291,12 +291,12 @@ https://siromove.com/invite.php?code=$couponCode&app=rider
|
||||
);
|
||||
} else {
|
||||
log('Permission DENIED', name: 'ContactPicker');
|
||||
mySnackeBarError('Contact permission is required to pick contacts'.tr);
|
||||
mySnackbarError('Contact permission is required to pick contacts'.tr);
|
||||
}
|
||||
} catch (e) {
|
||||
if (Get.isDialogOpen ?? false) Get.back();
|
||||
log('CRITICAL ERROR: $e', name: 'ContactPicker');
|
||||
mySnackeBarError('An error occurred while loading contacts: $e'.tr);
|
||||
mySnackbarError('An error occurred while loading contacts: $e'.tr);
|
||||
}
|
||||
log('=== END: FETCHING CONTACTS ===', name: 'ContactPicker');
|
||||
}
|
||||
@@ -383,7 +383,7 @@ https://siromove.com/invite.php?code=$couponCode&app=rider
|
||||
// refresh stats
|
||||
fetchDriverStats();
|
||||
} else {
|
||||
mySnackeBarError(data['message'] ?? 'Claim failed'.tr);
|
||||
mySnackbarError(data['message'] ?? 'Claim failed'.tr);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -475,13 +475,13 @@ https://siromove.com/invite.php?code=$couponCode&app=rider
|
||||
/// Sends an invitation to a potential new driver.
|
||||
void sendInvite() async {
|
||||
if (invitePhoneController.text.isEmpty) {
|
||||
mySnackeBarError('Please enter a phone number'.tr);
|
||||
mySnackbarError('Please enter a phone number'.tr);
|
||||
return;
|
||||
}
|
||||
String formattedPhoneNumber =
|
||||
CountryLogic.formatCurrentCountryPhone(invitePhoneController.text);
|
||||
if (formattedPhoneNumber.length != 12) {
|
||||
mySnackeBarError('Please enter a correct phone'.tr);
|
||||
mySnackbarError('Please enter a correct phone'.tr);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -507,14 +507,14 @@ https://siromove.com/invite.php?code=$couponCode&app=rider
|
||||
launchCommunication('whatsapp', formattedPhoneNumber, message);
|
||||
invitePhoneController.clear();
|
||||
} else {
|
||||
mySnackeBarError("Invite code already used".tr);
|
||||
mySnackbarError("Invite code already used".tr);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends an invitation to a potential new passenger.
|
||||
void sendInviteToPassenger() async {
|
||||
if (invitePhoneController.text.isEmpty) {
|
||||
mySnackeBarError('Please enter a phone number'.tr);
|
||||
mySnackbarError('Please enter a phone number'.tr);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -522,7 +522,7 @@ https://siromove.com/invite.php?code=$couponCode&app=rider
|
||||
|
||||
if (formattedPhoneNumber.length < 12) {
|
||||
// +963 + 9 digits = 12+
|
||||
mySnackeBarError('Please enter a correct phone'.tr);
|
||||
mySnackbarError('Please enter a correct phone'.tr);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -552,7 +552,7 @@ https://siromove.com/invite.php?code=$couponCode&app=rider
|
||||
launchCommunication('whatsapp', formattedPhoneNumber, message);
|
||||
invitePhoneController.clear();
|
||||
} else {
|
||||
mySnackeBarError("Invite code already used".tr);
|
||||
mySnackbarError("Invite code already used".tr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -605,17 +605,17 @@ class LoginDriverController extends GetxController {
|
||||
|
||||
Get.off(() => HomeCaptain());
|
||||
} else {
|
||||
mySnackeBarError('Login failed'.tr);
|
||||
mySnackbarError('Login failed'.tr);
|
||||
isloading = false;
|
||||
update();
|
||||
}
|
||||
} else {
|
||||
mySnackeBarError('Server error'.tr);
|
||||
mySnackbarError('Server error'.tr);
|
||||
isloading = false;
|
||||
update();
|
||||
}
|
||||
} catch (e) {
|
||||
mySnackeBarError('Network error'.tr);
|
||||
mySnackbarError('Network error'.tr);
|
||||
isloading = false;
|
||||
update();
|
||||
}
|
||||
|
||||
@@ -116,10 +116,10 @@ class OtpVerificationController extends GetxController {
|
||||
|
||||
Get.offAll(() => HomeCaptain());
|
||||
} else {
|
||||
mySnackeBarError('OTP is incorrect or expired'.tr);
|
||||
mySnackbarError('OTP is incorrect or expired'.tr);
|
||||
}
|
||||
} catch (e) {
|
||||
mySnackeBarError(e.toString());
|
||||
mySnackbarError(e.toString());
|
||||
} finally {
|
||||
isVerifying.value = false;
|
||||
}
|
||||
|
||||
@@ -44,15 +44,15 @@ class PhoneAuthHelper {
|
||||
mySnackbarSuccess('An OTP has been sent to your number.'.tr);
|
||||
return true;
|
||||
} else {
|
||||
mySnackeBarError(data['message'] ?? 'Failed to send OTP.'.tr);
|
||||
mySnackbarError(data['message'] ?? 'Failed to send OTP.'.tr);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
mySnackeBarError('Server error. Please try again.'.tr);
|
||||
mySnackbarError('Server error. Please try again.'.tr);
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
// mySnackeBarError('An error occurred: $e');
|
||||
// mySnackbarError('An error occurred: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -99,13 +99,13 @@ class PhoneAuthHelper {
|
||||
Get.to(() => RegistrationView());
|
||||
}
|
||||
} else {
|
||||
mySnackeBarError(data['message'] ?? 'Verification failed.');
|
||||
mySnackbarError(data['message'] ?? 'Verification failed.');
|
||||
}
|
||||
} else {
|
||||
mySnackeBarError('Server error. Please try again.');
|
||||
mySnackbarError('Server error. Please try again.');
|
||||
}
|
||||
} catch (e) {
|
||||
mySnackeBarError('An error occurred: $e');
|
||||
mySnackbarError('An error occurred: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,11 +130,11 @@ class PhoneAuthHelper {
|
||||
// Registration successful, log user in
|
||||
await _handleSuccessfulLogin(data['message']);
|
||||
} else {
|
||||
mySnackeBarError(
|
||||
mySnackbarError(
|
||||
"User with this phone number or email already exists.".tr);
|
||||
}
|
||||
} catch (e) {
|
||||
mySnackeBarError('An error occurred: $e');
|
||||
mySnackbarError('An error occurred: $e');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ class RegisterCaptainController extends GetxController {
|
||||
update();
|
||||
}
|
||||
} else {
|
||||
mySnackeBarError(
|
||||
mySnackbarError(
|
||||
'Phone Number wrong'.tr,
|
||||
);
|
||||
}
|
||||
@@ -285,7 +285,7 @@ class RegisterCaptainController extends GetxController {
|
||||
// }
|
||||
}
|
||||
} else {
|
||||
mySnackeBarError('you must insert token code '.tr);
|
||||
mySnackbarError('you must insert token code '.tr);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -220,7 +220,7 @@ class RegistrationController extends GetxController {
|
||||
// // الإرسال للذكاء الاصطناعي
|
||||
// await sendToAI(type, imageFile: outFile);
|
||||
} catch (e) {
|
||||
mySnackeBarError('${'An unexpected error occurred:'.tr} $e');
|
||||
mySnackbarError('${'An unexpected error occurred:'.tr} $e');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,7 +344,7 @@ class RegistrationController extends GetxController {
|
||||
Log.print('✅ Uploaded $imageType => $imageUrl');
|
||||
} catch (e, st) {
|
||||
Log.print('❌ Error in choosImage: $e\n$st');
|
||||
mySnackeBarError('Image Upload Failed'.tr);
|
||||
mySnackbarError('Image Upload Failed'.tr);
|
||||
} finally {
|
||||
isloading = false;
|
||||
update();
|
||||
@@ -717,10 +717,10 @@ class RegistrationController extends GetxController {
|
||||
c.loginDriver(driverID, email);
|
||||
} else {
|
||||
final msg = (json?['message'] ?? 'Registration failed.').toString();
|
||||
mySnackeBarError(msg);
|
||||
mySnackbarError(msg);
|
||||
}
|
||||
} catch (e) {
|
||||
mySnackeBarError('Error: $e');
|
||||
mySnackbarError('Error: $e');
|
||||
} finally {
|
||||
client.close();
|
||||
isLoading.value = false;
|
||||
|
||||
@@ -146,7 +146,7 @@ class CRUD {
|
||||
} on SocketException catch (_) {
|
||||
Log.print('⚠️ SocketException attempt $attempts — $link');
|
||||
if (attempts >= 3) {
|
||||
_netGuard.notifyOnce((title, msg) => mySnackeBarError(msg));
|
||||
_netGuard.notifyOnce((title, msg) => mySnackbarError(msg));
|
||||
return 'no_internet';
|
||||
}
|
||||
await Future.delayed(Duration(seconds: attempts));
|
||||
|
||||
@@ -122,7 +122,7 @@ class AI extends GetxController {
|
||||
driverList: [], category: 'You have received a gift token!',
|
||||
);
|
||||
} else {
|
||||
// mySnackeBarError(
|
||||
// mySnackbarError(
|
||||
// "You dont have invitation code".tr); // Localized error message
|
||||
}
|
||||
}
|
||||
@@ -137,7 +137,7 @@ class AI extends GetxController {
|
||||
update();
|
||||
mySnackbarSuccess("Code approved".tr);
|
||||
} else {
|
||||
mySnackeBarError("Code not approved".tr);
|
||||
mySnackbarError("Code not approved".tr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -313,12 +313,12 @@ class AI extends GetxController {
|
||||
'${box.read(BoxName.phoneDriver)}${Env.email}');
|
||||
mySnackbarSuccess('Driver data saved successfully');
|
||||
} else {
|
||||
mySnackeBarError('${'Failed to save driver data'.tr}: }');
|
||||
mySnackbarError('${'Failed to save driver data'.tr}: }');
|
||||
}
|
||||
} catch (e) {
|
||||
isLoading = false;
|
||||
update();
|
||||
mySnackeBarError(
|
||||
mySnackbarError(
|
||||
'An error occurred while saving driver data'.tr,
|
||||
);
|
||||
}
|
||||
@@ -1020,7 +1020,7 @@ class AI extends GetxController {
|
||||
|
||||
update();
|
||||
} else {
|
||||
mySnackeBarError("JSON string not found");
|
||||
mySnackbarError("JSON string not found");
|
||||
}
|
||||
|
||||
// Rest of your code...
|
||||
|
||||
@@ -77,7 +77,7 @@ class LogOutController extends GetxController {
|
||||
var id = await checkBeforeDelete();
|
||||
deleteMyAccountDriver(id);
|
||||
} else {
|
||||
mySnackeBarError('Your Name is Wrong'.tr);
|
||||
mySnackbarError('Your Name is Wrong'.tr);
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -148,7 +148,7 @@ class LogOutController extends GetxController {
|
||||
'email': box.read(BoxName.email),
|
||||
});
|
||||
} else {
|
||||
mySnackeBarError(
|
||||
mySnackbarError(
|
||||
'Email you inserted is Wrong.'.tr,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ class ImageController extends GetxController {
|
||||
);
|
||||
} catch (e) {
|
||||
print('Error in choosImage: $e');
|
||||
mySnackeBarError('Image Upload Failed'.tr);
|
||||
mySnackbarError('Image Upload Failed'.tr);
|
||||
// Get.snackbar('Image Upload Failed'.tr, e.toString(),
|
||||
// backgroundColor: AppColor.primaryColor);
|
||||
} finally {
|
||||
@@ -218,7 +218,7 @@ class ImageController extends GetxController {
|
||||
print('Error in choosImage: $e');
|
||||
// Get.snackbar('Image Upload Failed'.tr, e.toString(),
|
||||
// backgroundColor: AppColor.primaryColor);
|
||||
mySnackeBarError('Image Upload Failed'.tr);
|
||||
mySnackbarError('Image Upload Failed'.tr);
|
||||
} finally {
|
||||
isloading = false;
|
||||
update();
|
||||
@@ -293,7 +293,7 @@ class ImageController extends GetxController {
|
||||
link,
|
||||
);
|
||||
} catch (e) {
|
||||
mySnackeBarError('Image Upload Failed'.tr);
|
||||
mySnackbarError('Image Upload Failed'.tr);
|
||||
// Get.snackbar('Image Upload Failed'.tr, e.toString(),
|
||||
// backgroundColor: AppColor.redColor);
|
||||
} finally {
|
||||
@@ -439,7 +439,7 @@ class ImageController extends GetxController {
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print('e: ${e}');
|
||||
mySnackeBarError('Image Upload Failed'.tr);
|
||||
mySnackbarError('Image Upload Failed'.tr);
|
||||
} finally {
|
||||
isloading = false;
|
||||
update();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user