Update: 2026-06-26 17:29:23

This commit is contained in:
Hamza-Ayed
2026-06-26 17:29:23 +03:00
parent a323da29aa
commit 9ded734e38
139 changed files with 1815 additions and 2676 deletions

View File

@@ -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>";
}

View File

@@ -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',

View File

@@ -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"]);
}

View File

@@ -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";
?>

View File

@@ -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.");

View File

@@ -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";
}

View File

@@ -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";
}

View File

@@ -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);
}

View File

@@ -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";
}
?>

View File

@@ -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());
}

View File

@@ -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";
}
?>

View 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.*

View File

@@ -1,6 +1,5 @@
import '../env/env.dart';
import '../main.dart';
import 'box_name.dart';
class AppLink {
static String seferPaymentServer =

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,8 +54,9 @@ 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 = {};
data.forEach((key, value) {
payload[key] = value.toString();
@@ -51,7 +69,7 @@ class KazanController extends GetxController {
}
return false;
} catch (e) {
Get.snackbar("خطأ", "فشل تحديث التسعير: $e");
mySnackbarError('فشل تحديث التسعير: $e');
return false;
} finally {
isLoading.value = false;

View File

@@ -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
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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
}
}

View File

@@ -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...

View File

@@ -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;

View File

@@ -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();

View File

@@ -303,10 +303,11 @@ 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 ────────────────────────────────────────────────
@@ -322,10 +323,11 @@ 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 ──────────────────────────────────────────────

View File

@@ -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
}
}

View File

@@ -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();

View File

@@ -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,8 +161,9 @@ 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);
@@ -197,7 +198,7 @@ class OtpHelper extends GetxController {
Get.back();
verifyLoginOtp(phone, otpCode, password, fingerprint);
} else {
mySnackeBarError('الرجاء إدخال رمز صحيح');
mySnackbarError('الرجاء إدخال رمز صحيح');
}
},
);

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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');
}
}
}

View File

@@ -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');
}
}

View File

@@ -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');
}
}

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}
}
}

View File

@@ -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';

View File

@@ -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()))}',

View File

@@ -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');
}
}
}

View File

@@ -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");
}
}

View File

@@ -3,7 +3,6 @@ import 'dart:async';
import 'package:get/get.dart';
import 'package:siro_admin/constant/links.dart';
import '../../print.dart';
// --- Models ---

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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)),
),

View File

@@ -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);
}

View File

@@ -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) {

View File

@@ -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';

View File

@@ -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(

View File

@@ -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(

View File

@@ -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('تم تفعيل حساب السائق بنجاح');
}
},
),

View File

@@ -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();

View File

@@ -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(),

View File

@@ -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,

View File

@@ -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)),
);
}

View File

@@ -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();

View File

@@ -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)),
);
}
}

View File

@@ -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();

View File

@@ -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,

View File

@@ -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,
),
),
),
]
],
),
],
),
),
);
}
}

View File

@@ -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('فشل التحديث، يرجى المحاولة مجدداً');
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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)),

View File

@@ -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,39 +18,77 @@ class KazanEditorPage extends StatelessWidget {
title: 'تعديل أسعار كازان'.tr,
isleading: true,
body: [
Obx(() => controller.isLoading.value && controller.kazanData.isEmpty
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
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,
onPressed: () => _handleSave(),
),
const SizedBox(height: 100),
],
),
)),
),
],
),
],
);
}
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: [
// ⚙️ الإعدادات العامة
_buildSectionHeader('⚙️ الإعدادات العامة'),
_buildGeneralSettings(),
const SizedBox(height: 24),
// 🚗 أسعار الكيلومتر لكل نوع سيارة
_buildSectionHeader('🚗 أسعار الكيلومتر لكل نوع سيارة'),
_buildKmPricesGrid(),
const SizedBox(height: 24),
// ⏱️ أسعار الدقيقة
_buildSectionHeader('⏱️ أسعار الدقيقة (حسب وقت اليوم)'),
_buildMinutePrices(),
const SizedBox(height: 32),
// 💾 زر الحفظ
MyElevatedButton(
title: '💾 حفظ جميع التعديلات',
icon: Icons.save_rounded,
onPressed: () => _handleSave(),
),
const SizedBox(height: 100),
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('فشل تحديث الأسعار');
}
}
}

View File

@@ -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);
},

View File

@@ -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: [

View File

@@ -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(

View File

@@ -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) {

View File

@@ -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(

View File

@@ -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');
}
}

View File

@@ -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 =

View File

@@ -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';

View File

@@ -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>(

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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,

View File

@@ -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());

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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';

View File

@@ -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';

View File

@@ -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),
blurRadius: 10,
offset: const Offset(0, 2),
);
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',
};
}
SnackbarController mySnackeBarError(String message) {
// Trigger error haptic feedback
HapticFeedback.mediumImpact();
class _SnackContent extends StatefulWidget {
final String message;
final _SnackVariant variant;
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,
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),
),
],
),
),
messageText: Text(
message,
style: TextStyle(
color: Colors.white.withOpacity(0.95),
fontSize: 14,
height: 1.3,
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),
),
),
),
),
],
),
),
],
),
),
),
onTap: (_) {
);
}
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();
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);

View File

@@ -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"

View File

@@ -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

View File

@@ -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);
}
}
}

View File

@@ -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();
}

View File

@@ -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;
}

View File

@@ -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');
}
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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));

View File

@@ -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...

View File

@@ -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,
);
}

View File

@@ -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