// earnings_controller.dart — مصدر واحد لأرقام شاشة الأرباح // // ‏كانت الأرقام تُبنى من أربعة نداءات مستقلة، فيصل كلٌّ منها في لحظة // ‏مختلفة وتظهر أرقام لا تتفق. هنا نداء واحد بختم زمني واحد. // // ‏والأهم: **الفشل لا يُترجم أصفاراً**. الكود السابق كان عند فشل الشبكة // ‏يولّد سبعة أيام بأصفار ويعرضها كأنها أرباح السائق — وهو يرى دخله وقد // ‏تبخّر. الحالتان هنا منفصلتان: `hasError` غير «كسبتَ صفراً». import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:siro_driver/constant/links.dart'; import 'package:siro_driver/controller/functions/crud.dart'; import 'package:siro_driver/main.dart'; import 'earnings_models.dart'; class EarningsController extends GetxController { EarningsSummary? summary; List trips = []; bool isLoading = false; bool isLoadingTrips = false; /// ‏سبب الفشل كنصّ للسائق، أو null. وجوده يعني «لا نعرف أرباحك الآن» /// ‏لا «أرباحك صفر» — والفرق بينهما هو الفرق بين عطلٍ يُحتمل وثقةٍ تُفقد. String? errorMessage; /// ‏هل المعروض من الذاكرة المؤقتة؟ الشاشة تُظهر ذلك صراحةً مع وقت /// ‏القياس، فلا يقرأ السائق رقماً عمره ساعات على أنه لحظته. bool isStale = false; DateTime? lastFetch; /// ‏اليوم المختار لقائمة الرحلات (null = آخر سبعة أيام). String? selectedDay; static const _cacheKey = 'earnings_summary_cache'; static const _cacheAtKey = 'earnings_summary_cached_at'; /// ‏دقيقتان لا ثلاث ساعات. الكاش الطويل السابق كان سبب تناقض شاشة /// ‏الأرباح مع شاشة الرصيد؛ ومدة قصيرة تكفي لمنع النداءات المتكررة /// ‏عند التنقّل بين الشاشات بلا أن تُقدّم للسائق ماضياً على أنه حاضر. static const _freshFor = Duration(minutes: 2); @override void onInit() { super.onInit(); _loadFromCache(); load(); } void _loadFromCache() { try { final raw = box.read(_cacheKey); if (raw == null) return; summary = EarningsSummary.fromJson( Map.from(jsonDecode(raw))); final at = box.read(_cacheAtKey); if (at != null) lastFetch = DateTime.tryParse(at.toString()); isStale = true; update(); } catch (e) { debugPrint('[Earnings] تعذّرت قراءة الكاش: $e'); } } Future load({bool force = false}) async { if (!force && lastFetch != null && !isStale) { if (DateTime.now().difference(lastFetch!) < _freshFor) return; } isLoading = summary == null; // ‏دوّار فقط حين لا يوجد ما يُعرض errorMessage = null; update(); final res = await CRUD().get(link: AppLink.getEarningsSummary); // ‏الرسائل مفصّلة بحسب سبب الفشل: «تحقق من اتصالك» تُصلح بفعل، // ‏و«حدث خطأ» تترك السائق بلا حيلة. if (res == 'no_internet') { _fail('تعذّر الوصول للخادم — تحقّق من اتصالك'); return; } if (res == 'token_expired') { _fail('انتهت الجلسة، حاول مجدداً'); return; } if (res == 'failure' || res == null) { _fail('تعذّر تحميل أرباحك الآن'); return; } try { final data = jsonDecode(res); if (data['status'] != 'success' || data['message'] is! Map) { _fail('تعذّر تحميل أرباحك الآن'); return; } summary = EarningsSummary.fromJson( Map.from(data['message'])); box.write(_cacheKey, jsonEncode(data['message'])); box.write(_cacheAtKey, DateTime.now().toIso8601String()); lastFetch = DateTime.now(); isStale = false; errorMessage = null; } catch (e) { debugPrint('[Earnings] تعذّر تحليل الرد: $e'); _fail('تعذّر تحميل أرباحك الآن'); return; } finally { isLoading = false; update(); } await loadTrips(); } /// ‏الفشل يُبقي آخر بيانات معروفة معروضة ومعلَّمة كقديمة، ولا يستبدلها /// ‏بأصفار. رقم قديم صادق أنفع من رقم حاضر كاذب. void _fail(String message) { errorMessage = message; isLoading = false; if (summary != null) isStale = true; update(); } Future loadTrips({String? day}) async { selectedDay = day; isLoadingTrips = true; update(); final res = await CRUD().get( link: AppLink.getEarningsTrips, payload: { if (day != null) 'date': day, if (day == null) 'range': 'week', }, ); try { if (res is String && res != 'failure' && res != 'no_internet' && res != 'token_expired') { final data = jsonDecode(res); if (data['status'] == 'success' && data['message'] is Map) { final list = data['message']['trips']; trips = (list is List) ? list .map((t) => TripEarning.fromJson(Map.from(t))) .toList() : []; } } } catch (e) { debugPrint('[Earnings] تعذّر تحليل الرحلات: $e'); } isLoadingTrips = false; update(); } /// ‏نصّ «آخر تحديث» بصيغة يفهمها السائق بلا حساب. String get lastUpdatedLabel { if (lastFetch == null) return ''; final diff = DateTime.now().difference(lastFetch!); if (diff.inMinutes < 1) return 'الآن'; if (diff.inMinutes < 60) return 'قبل ${diff.inMinutes} دقيقة'; if (diff.inHours < 24) return 'قبل ${diff.inHours} ساعة'; return 'قبل ${diff.inDays} يوم'; } }