Update: 2026-08-08 12:58:16

This commit is contained in:
Hamza-Ayed
2026-08-08 12:58:17 +03:00
parent 611b5e616d
commit fae0e4a38a
55 changed files with 5771 additions and 1820 deletions
@@ -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} يوم';
}
}