Update: 2026-08-08 12:58:16
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
// 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<TripEarning> 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<String, dynamic>.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<void> 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<String, dynamic>.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<void> 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<String, dynamic>.from(t)))
|
||||
.toList()
|
||||
: <TripEarning>[];
|
||||
}
|
||||
}
|
||||
} 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} يوم';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user