diff --git a/backend/ride/gamification/getLeaderboard.php b/backend/ride/gamification/getLeaderboard.php index 29408c66..543323ba 100644 --- a/backend/ride/gamification/getLeaderboard.php +++ b/backend/ride/gamification/getLeaderboard.php @@ -28,14 +28,17 @@ try { $sql = " SELECT d.id as driver_id, - COALESCE(d.name, d.nameArabic, d.firstName, 'Driver') as name, - d.personal_photo as photoUrl, + -- ‏الأعمدة الفعلية في جدول driver. الاستعلام السابق كان + -- ‏يطلب name / nameArabic / firstName / personal_photo وأيٌّ + -- ‏منها غير موجود، فكان كل طلب يسقط بخطأ قاعدة بيانات. + d.first_name as first_name, + d.last_name as last_name, COALESCE(SUM(r.price_for_driver), 0) as value FROM `driver` d JOIN `ride` r ON d.id = r.driver_id WHERE r.status = 'Finished' AND r.created_at >= DATE(NOW() - INTERVAL WEEKDAY(NOW()) DAY) - GROUP BY d.id + GROUP BY d.id, d.first_name, d.last_name ORDER BY value DESC LIMIT 10 "; @@ -44,14 +47,17 @@ try { $sql = " SELECT d.id as driver_id, - COALESCE(d.name, d.nameArabic, d.firstName, 'Driver') as name, - d.personal_photo as photoUrl, + -- ‏الأعمدة الفعلية في جدول driver. الاستعلام السابق كان + -- ‏يطلب name / nameArabic / firstName / personal_photo وأيٌّ + -- ‏منها غير موجود، فكان كل طلب يسقط بخطأ قاعدة بيانات. + d.first_name as first_name, + d.last_name as last_name, COUNT(r.id) as value FROM `driver` d JOIN `ride` r ON d.id = r.driver_id WHERE r.status = 'Finished' AND r.created_at >= DATE(NOW() - INTERVAL WEEKDAY(NOW()) DAY) - GROUP BY d.id + GROUP BY d.id, d.first_name, d.last_name ORDER BY value DESC LIMIT 10 "; @@ -71,8 +77,20 @@ if ($stmt->rowCount() > 0) { $rank = 1; foreach ($rows as &$row) { $row['rank'] = $rank++; + + // ‏الأسماء مخزّنة مشفّرة في driver — بلا فكّ تشفير تظهر اللوحة + // ‏صفوفاً من النص المشفّر. نعرض الاسم الأول فقط: لوحة صدارة + // ‏عامة لا داعي لأن تكشف الاسم الكامل لكل سائق. + $first = !empty($row['first_name']) + ? (string) $encryptionHelper->decryptData($row['first_name']) + : ''; + unset($row['first_name'], $row['last_name']); + + $row['name'] = $first !== '' ? $first : 'Driver'; + $row['photoUrl'] = null; } - + unset($row); + $responseData = $rows; } else { $responseData = []; diff --git a/backend/ride/invitor/get_driver_referrals.php b/backend/ride/invitor/get_driver_referrals.php index 12c4c7b4..09a50aa0 100644 --- a/backend/ride/invitor/get_driver_referrals.php +++ b/backend/ride/invitor/get_driver_referrals.php @@ -1,17 +1,18 @@ prepare("SELECT referral_code FROM user_referral_codes WHERE user_id = ? AND user_type = 'driver'"); -$stmtCode->execute([$user_id]); +// 1. Get the driver's referral code — ونُنشئه إن غاب. +// ‏الردّ بـ null كان يترك شاشة الدعوة تدور بلا كود إلى الأبد، لأن لا +// ‏شيء في التطبيق ينادي get_unified_code.php الذي يولّده. +$referralCode = ensureReferralCode($con, (string) $user_id, 'driver'); -if ($stmtCode->rowCount() == 0) { - // If no code exists, return empty stats +if ($referralCode === null) { printSuccess([ "referral_code" => null, "total_invited_drivers" => 0, @@ -21,8 +22,6 @@ if ($stmtCode->rowCount() == 0) { exit; } -$referralCode = $stmtCode->fetchColumn(); - // 2. Fetch all referrals made by this code $stmtRefs = $con->prepare(" SELECT id, invited_user_id, invited_user_type, status, trip_count, is_reward_claimed, created_at diff --git a/backend/ride/invitor/get_passenger_referrals.php b/backend/ride/invitor/get_passenger_referrals.php index 63e30445..34eedb89 100644 --- a/backend/ride/invitor/get_passenger_referrals.php +++ b/backend/ride/invitor/get_passenger_referrals.php @@ -1,5 +1,6 @@ prepare("SELECT referral_code FROM user_referral_codes WHERE user_id = ? AND user_type = 'passenger'"); -$stmtCode->execute([$user_id]); +// 1. Get the passenger's referral code — ونُنشئه إن غاب (نفس علّة السائق). +$referralCode = ensureReferralCode($con, (string) $user_id, 'passenger'); -if ($stmtCode->rowCount() == 0) { - // If no code exists, return empty stats +if ($referralCode === null) { printSuccess([ "referral_code" => null, "total_invited_drivers" => 0, @@ -23,8 +22,6 @@ if ($stmtCode->rowCount() == 0) { exit; } -$referralCode = $stmtCode->fetchColumn(); - // 2. Fetch all referrals made by this code $stmtRefs = $con->prepare(" SELECT id, invited_user_id, invited_user_type, status, trip_count, is_reward_claimed, created_at diff --git a/backend/ride/invitor/get_unified_code.php b/backend/ride/invitor/get_unified_code.php index c74368a7..00dbd012 100644 --- a/backend/ride/invitor/get_unified_code.php +++ b/backend/ride/invitor/get_unified_code.php @@ -1,42 +1,19 @@ prepare("SELECT COUNT(*) FROM user_referral_codes WHERE referral_code = ?"); - $stmt->execute([$code]); - - if ($stmt->fetchColumn() == 0) { - return $code; - } - } -} +// ‏منطق التوليد انتقل إلى helper مشترك حتى تولّده نقاط القراءة أيضاً — +// ‏هذه النقطة لم تكن مُستدعاة من أي شاشة، فبقي الكود بلا توليد. +require_once __DIR__ . '/referral_code_helper.php'; // Ensure the JWT values exist if (!$user_id || !in_array($role, ['driver', 'passenger'])) { jsonError("Invalid or missing user information in token"); } -$stmt = $con->prepare("SELECT referral_code FROM user_referral_codes WHERE user_id = ? AND user_type = ?"); -$stmt->execute([$user_id, $role]); +$code = ensureReferralCode($con, (string) $user_id, (string) $role); -if ($stmt->rowCount() > 0) { - $row = $stmt->fetch(PDO::FETCH_ASSOC); - printSuccess(["referral_code" => $row['referral_code']]); -} else { - $newCode = generateUnifiedCode($con); - $insertStmt = $con->prepare("INSERT INTO user_referral_codes (user_id, user_type, referral_code) VALUES (?, ?, ?)"); - - try { - $insertStmt->execute([$user_id, $role, $newCode]); - printSuccess(["referral_code" => $newCode]); - } catch (PDOException $e) { - error_log("[get_unified_code.php] " . $e->getMessage()); - jsonError("An internal error occurred. Please try again later."); - } +if ($code === null) { + jsonError("An internal error occurred. Please try again later."); } -?> + +printSuccess(["referral_code" => $code]); + diff --git a/backend/ride/invitor/referral_code_helper.php b/backend/ride/invitor/referral_code_helper.php new file mode 100644 index 00000000..dc194869 --- /dev/null +++ b/backend/ride/invitor/referral_code_helper.php @@ -0,0 +1,68 @@ +prepare("SELECT COUNT(*) FROM user_referral_codes WHERE referral_code = ?"); + $stmt->execute([$code]); + if ((int) $stmt->fetchColumn() === 0) { + return $code; + } + } + // ‏احتياط: حلقة while اللانهائية الأصلية كانت تُعلّق العامل كاملاً + // ‏لو امتلأت مساحة الأكواد. نسقط لكود أطول بدل التعليق. + return substr(strtoupper(bin2hex(random_bytes(4))), 0, 8); + } +} + +if (!function_exists('ensureReferralCode')) { + /** + * ‏يعيد كود الدعوة للمستخدم، ويُنشئه إن لم يكن موجوداً. + * ‏يعيد null فقط إذا تعذّرت الكتابة (لا يُسقط الطلب). + */ + function ensureReferralCode(PDO $con, string $userId, string $userType): ?string + { + $stmt = $con->prepare("SELECT referral_code FROM user_referral_codes WHERE user_id = ? AND user_type = ?"); + $stmt->execute([$userId, $userType]); + $existing = $stmt->fetchColumn(); + if ($existing !== false && $existing !== null && $existing !== '') { + return (string) $existing; + } + + try { + $newCode = generateUnifiedCode($con); + $con->prepare("INSERT INTO user_referral_codes (user_id, user_type, referral_code) VALUES (?, ?, ?)") + ->execute([$userId, $userType, $newCode]); + return $newCode; + } catch (PDOException $e) { + // ‏سباق بين طلبين متزامنين: الآخر كتب الصفّ أولاً — نقرأه. + $stmt->execute([$userId, $userType]); + $raced = $stmt->fetchColumn(); + if ($raced !== false && $raced !== null && $raced !== '') { + return (string) $raced; + } + error_log("[referral_code_helper] " . $e->getMessage()); + return null; + } + } +} diff --git a/siro_driver/lib/controller/home/captin/destination_controller.dart b/siro_driver/lib/controller/home/captin/destination_controller.dart index bdae0b3b..067d0b9d 100644 --- a/siro_driver/lib/controller/home/captin/destination_controller.dart +++ b/siro_driver/lib/controller/home/captin/destination_controller.dart @@ -45,7 +45,10 @@ class DestinationController extends GetxController { if (response != null && response is Map && response['status'] == 'success') { final data = response['message']; - if (data != null) { + // عند غياب وجهة نشطة يردّ السيرفر نصاً ("No active destination set.") + // لا خريطة، وفهرسته كخريطة كان يرمي + // type 'String' is not a subtype of type 'int'. + if (data is Map) { final lat = double.tryParse(data['target_latitude']?.toString() ?? '0') ?? 0.0; final lng = double.tryParse(data['target_longitude']?.toString() ?? '0') ?? 0.0; activeLatLng = LatLng(lat, lng); diff --git a/siro_driver/lib/controller/home/payment/captain_wallet_controller.dart b/siro_driver/lib/controller/home/payment/captain_wallet_controller.dart index 0bb2ce9f..37a992cd 100755 --- a/siro_driver/lib/controller/home/payment/captain_wallet_controller.dart +++ b/siro_driver/lib/controller/home/payment/captain_wallet_controller.dart @@ -10,6 +10,7 @@ import 'package:siro_driver/constant/box_name.dart'; import 'package:siro_driver/constant/links.dart'; import 'package:siro_driver/controller/functions/crud.dart'; import 'package:siro_driver/main.dart'; +import 'package:siro_driver/print.dart'; import 'package:siro_driver/views/widgets/mycircular.dart'; import '../../../views/home/my_wallet/payment_screen_mtn.dart'; @@ -88,9 +89,36 @@ class CaptainWalletController extends GetxController { } Future refreshCaptainWallet() async { - await getCaptainWalletFromRide(); - await getCaptainWalletFromBuyPoints(); - // await checkAccountCaptainBank(); + // ═══════════════════════════════════════════════════════════════ + // ‏isLoading كان يُطفأ في نهاية الدالة الثانية فقط. أي استثناء قبلها + // ‏— وأشهره سائق بلا أي دفعات، إذ يعود message = [] فيرمي فهرسه + // ‏[0] خطأ نطاق — كان يترك الشاشة تدور بلا نهاية بلا رسالة. + // ‏finally يضمن إطفاءه مهما حدث. + // ═══════════════════════════════════════════════════════════════ + isLoading = true; + update(); + try { + await getCaptainWalletFromRide(); + await getCaptainWalletFromBuyPoints(); + // await checkAccountCaptainBank(); + } catch (e) { + Log.print('❌ refreshCaptainWallet: $e'); + mySnackbarError('Connection problem, please try again'.tr); + } finally { + isLoading = false; + update(); + } + } + + /// ‏يقرأ رقماً من ردّ المحفظة بأمان: الردّ قد يكون خريطة بلا `message`، + /// ‏أو قائمة فارغة (سائق جديد بلا دفعات) — وكلاهما كان يُسقط الشاشة. + static String _readWalletNumber(dynamic decoded, String key) { + if (decoded is! Map) return '0'; + final message = decoded['message']; + if (message is! List || message.isEmpty) return '0'; + final first = message.first; + if (first is! Map) return '0'; + return first[key]?.toString() ?? '0'; } List amountToNewDriverMap = []; @@ -122,23 +150,27 @@ class CaptainWalletController extends GetxController { } Future getCaptainWalletFromRide() async { - isLoading = true; - update(); var res = await CRUD().getWallet( link: AppLink.getAllPaymentFromRide, payload: {'driverID': box.read(BoxName.driverID).toString()}, ); - // isLoading = false; + if (res != 'failure') { - walletDate = jsonDecode(res); - totalAmount = walletDate['message'][0]['total_amount'] ?? '0'; + final decoded = jsonDecode(res); + walletDate = decoded is Map ? decoded : {}; + totalAmount = _readWalletNumber(decoded, 'total_amount'); update(); + var res1 = await CRUD().getWallet( link: AppLink.getAllPaymentVisa, payload: {'driverID': box.read(BoxName.driverID).toString()}); - walletDateVisa = jsonDecode(res1); - totalAmountVisa = walletDateVisa['message'][0]['diff'].toString(); - + if (res1 != 'failure') { + final decodedVisa = jsonDecode(res1); + walletDateVisa = decodedVisa is Map ? decodedVisa : {}; + totalAmountVisa = _readWalletNumber(decodedVisa, 'diff'); + } else { + totalAmountVisa = '0'; + } update(); } else { totalAmount = "0"; @@ -147,21 +179,16 @@ class CaptainWalletController extends GetxController { } Future getCaptainWalletFromBuyPoints() async { - // isLoading = true; - update(); - var res = await CRUD().getWallet( link: AppLink.getDriverPaymentPoints, payload: {'driverID': box.read(BoxName.driverID).toString()}, ); - isLoading = false; - // update(); if (res != 'failure') { - walletDriverPointsDate = jsonDecode(res); - double totalPointsDouble = double.parse( - walletDriverPointsDate['message'][0]['total_amount'].toString()); - totalPoints = totalPointsDouble.toStringAsFixed(0); + final decoded = jsonDecode(res); + walletDriverPointsDate = decoded is Map ? decoded : {}; + final raw = _readWalletNumber(decoded, 'total_amount'); + totalPoints = (double.tryParse(raw) ?? 0).toStringAsFixed(0); } else { totalPoints = '0'; } diff --git a/siro_driver/lib/controller/scheduled/driver_scheduled_rides_controller.dart b/siro_driver/lib/controller/scheduled/driver_scheduled_rides_controller.dart index c0926499..d375d7c5 100644 --- a/siro_driver/lib/controller/scheduled/driver_scheduled_rides_controller.dart +++ b/siro_driver/lib/controller/scheduled/driver_scheduled_rides_controller.dart @@ -10,6 +10,11 @@ class DriverScheduledRidesController extends GetxController { final bookings = >[].obs; final isLoading = false.obs; + /// ‏فشل الجلب ≠ لا توجد حجوزات. بدون هذا العلم كانت الشاشة تعرض + /// ‏"لا حجوزات قادمة" حتى حين يردّ السيرفر 500، فيطمئن الكابتن + /// ‏لعدم وجود عمل بينما الحقيقة أننا لا نعرف. + final hasError = false.obs; + @override void onInit() { super.onInit(); @@ -21,13 +26,19 @@ class DriverScheduledRidesController extends GetxController { try { final res = await _crud.post( link: AppLink.scheduledList, - payload: {'scope': 'upcoming_driver'}, + payload: {'scope': 'upcoming_driver'}, ); final data = _extract(res); - final list = (data?['bookings'] as List?) ?? []; + if (data == null) { + hasError.value = true; + return; + } + final list = (data['bookings'] as List?) ?? []; bookings.assignAll( list.map((e) => Map.from(e as Map)).toList()); + hasError.value = false; } catch (e) { + hasError.value = true; mySnackbarError('Failed to fetch scheduled rides'.tr); } finally { isLoading.value = false; diff --git a/siro_driver/lib/translations_ar.json b/siro_driver/lib/translations_ar.json index ba09fb19..debd8f04 100644 --- a/siro_driver/lib/translations_ar.json +++ b/siro_driver/lib/translations_ar.json @@ -945,6 +945,10 @@ "Sun": "أحد", "Upcoming Scheduled Rides": "الرحلات المجدولة القادمة", "You have no upcoming scheduled rides": "ليس لديك رحلات مجدولة قادمة", + "Could not load scheduled rides": "تعذّر تحميل الرحلات المجدولة", + "Please check your connection and try again.": "تحقّق من اتصالك ثم أعد المحاولة.", + "Retry": "إعادة المحاولة", + "Insurance plans have not been launched in your area yet. Once a partner plan is available it will appear here automatically.": "لم تُطلق خطط التأمين في منطقتك بعد. ستظهر هنا تلقائياً فور توفّر خطة من أحد الشركاء.", "Starting Point": "نقطة الانطلاق", "Destination": "وجهة الوصول", "Failed to fetch scheduled rides": "تعذّر جلب الرحلات المجدولة", diff --git a/siro_driver/lib/translations_en.json b/siro_driver/lib/translations_en.json index 8f1bcd83..34c691c6 100644 --- a/siro_driver/lib/translations_en.json +++ b/siro_driver/lib/translations_en.json @@ -903,6 +903,10 @@ "Sun": "Sun", "Upcoming Scheduled Rides": "Upcoming Scheduled Rides", "You have no upcoming scheduled rides": "You have no upcoming scheduled rides", + "Could not load scheduled rides": "Could not load scheduled rides", + "Please check your connection and try again.": "Please check your connection and try again.", + "Retry": "Retry", + "Insurance plans have not been launched in your area yet. Once a partner plan is available it will appear here automatically.": "Insurance plans have not been launched in your area yet. Once a partner plan is available it will appear here automatically.", "Starting Point": "Starting Point", "Destination": "Destination", "Failed to fetch scheduled rides": "Failed to fetch scheduled rides", diff --git a/siro_driver/lib/views/home/Captin/home_captain/home_captin.dart b/siro_driver/lib/views/home/Captin/home_captain/home_captin.dart index 01a7fa0c..b818a67e 100755 --- a/siro_driver/lib/views/home/Captin/home_captain/home_captin.dart +++ b/siro_driver/lib/views/home/Captin/home_captain/home_captin.dart @@ -222,24 +222,34 @@ class _HomeAppBar extends StatelessWidget implements PreferredSizeWidget { // ── Logo + App Name ────────────────────── title: Padding( padding: const EdgeInsets.only(left: 4), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - _LogoBadge(), - const SizedBox(width: 6), - Flexible( - child: Text( - AppInformation.appName.split(' ')[0].tr, - style: const TextStyle( - color: _Token.accent, - fontSize: 18, - fontWeight: FontWeight.w800, - letterSpacing: 0.5, - ), - overflow: TextOverflow.ellipsis, - ), - ), - ], + // ‏شارات الـ actions تلتهم عرض الـ AppBar فلا يبقى للعنوان أحياناً + // ‏سوى ~26px، بينما الشعار وحده 36px — ومن هنا كان الفيض. + // ‏نُسقط الاسم عند ضيق المساحة ونُبقي الشعار: هو الهوية الأهم. + child: LayoutBuilder( + builder: (context, constraints) { + final showName = constraints.maxWidth >= 96; + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + _LogoBadge(), + if (showName) ...[ + const SizedBox(width: 6), + Flexible( + child: Text( + AppInformation.appName.split(' ')[0].tr, + style: const TextStyle( + color: _Token.accent, + fontSize: 18, + fontWeight: FontWeight.w800, + letterSpacing: 0.5, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ], + ); + }, ), ), actions: [ diff --git a/siro_driver/lib/views/home/journal/driver_insurance_page.dart b/siro_driver/lib/views/home/journal/driver_insurance_page.dart index 2b04c3f2..b91245ef 100644 --- a/siro_driver/lib/views/home/journal/driver_insurance_page.dart +++ b/siro_driver/lib/views/home/journal/driver_insurance_page.dart @@ -140,14 +140,39 @@ class DriverInsurancePage extends StatelessWidget { // ── الخطط المتاحة ────────────────────────────────────────── List _plansView(BuildContext context, DriverInsuranceController c) { if (c.plans.isEmpty) { + // ‏الصفحة كانت تبدو "معطّلة" لأنها تعرض عنواناً واحداً بلا سبب. + // ‏الحالة الحقيقية: لا خطط مُفعّلة بعد لدى الشركاء — لا خطأ ولا + // ‏نقص أهلية — ونقولها صراحة حتى لا يظن الكابتن أن التطبيق خربان. return [ - const SizedBox(height: 80), + const SizedBox(height: 72), Icon(Icons.health_and_safety_outlined, size: 80, color: Colors.grey.shade400), const SizedBox(height: 16), Text('No insurance plans available yet'.tr, textAlign: TextAlign.center, - style: const TextStyle(fontSize: 18, color: Colors.grey)), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: Colors.grey)), + const SizedBox(height: 10), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Text( + 'Insurance plans have not been launched in your area yet. Once a partner plan is available it will appear here automatically.' + .tr, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, height: 1.5, color: Colors.grey.shade600), + ), + ), + const SizedBox(height: 20), + Center( + child: TextButton.icon( + onPressed: c.isLoading.value ? null : () => c.refreshAll(), + icon: const Icon(Icons.refresh_rounded, size: 18), + label: Text('Refresh'.tr), + ), + ), ]; } diff --git a/siro_driver/lib/views/home/journal/driver_scheduled_rides_page.dart b/siro_driver/lib/views/home/journal/driver_scheduled_rides_page.dart index cabbd104..337aab49 100644 --- a/siro_driver/lib/views/home/journal/driver_scheduled_rides_page.dart +++ b/siro_driver/lib/views/home/journal/driver_scheduled_rides_page.dart @@ -20,6 +20,44 @@ class DriverScheduledRidesPage extends StatelessWidget { return const Center(child: CircularProgressIndicator()); } + // ‏تعذّر الجلب: نقولها بدل ادّعاء "لا حجوزات" — الفرق بينهما + // ‏هو الفرق بين كابتن يطمئن وكابتن يفوته عمل مؤكد. + if (controller.hasError.value) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.cloud_off_rounded, + size: 80, color: Colors.grey.shade400), + const SizedBox(height: 16), + Text( + 'Could not load scheduled rides'.tr, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: Colors.grey), + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Text( + 'Please check your connection and try again.'.tr, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, color: Colors.grey.shade600, height: 1.5), + ), + ), + const SizedBox(height: 16), + ElevatedButton.icon( + onPressed: controller.fetch, + icon: const Icon(Icons.refresh_rounded, size: 18), + label: Text('Retry'.tr), + ), + ], + ), + ); + } + if (controller.bookings.isEmpty) { return Center( child: Column( diff --git a/siro_driver/lib/views/home/statistics/widgets/stat_summary_card.dart b/siro_driver/lib/views/home/statistics/widgets/stat_summary_card.dart index 79eb1726..3a08f8c2 100644 --- a/siro_driver/lib/views/home/statistics/widgets/stat_summary_card.dart +++ b/siro_driver/lib/views/home/statistics/widgets/stat_summary_card.dart @@ -18,7 +18,9 @@ class StatSummaryCard extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - padding: const EdgeInsets.all(16), + // ‏رأسي 12 لا 16: البطاقة داخل شبكة بارتفاع ثابت، و16 كانت تتجاوزه + // ‏بنحو 10px مع خط الرقم مقاس 22. + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: FinanceDesignSystem.cardColor, borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius), @@ -54,14 +56,22 @@ class StatSummaryCard extends StatelessWidget { ), Column( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ - Text( - value, - style: TextStyle( - fontSize: 22, - fontWeight: FontWeight.w900, - color: FinanceDesignSystem.primaryDark, - fontFamily: 'digit', + // ‏الرقم يتقلّص بدل أن يفيض: مبالغ الأرباح تطول بلا سقف + // ‏(٤ خانات + كسور + عملة) والبطاقة عرضها ثابت. + FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.centerStart, + child: Text( + value, + maxLines: 1, + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.w900, + color: FinanceDesignSystem.primaryDark, + fontFamily: 'digit', + ), ), ), Text(