177 lines
6.2 KiB
Dart
177 lines
6.2 KiB
Dart
// 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} يوم';
|
|
}
|
|
}
|