// earnings_page.dart — شاشة أرباح السائق // // ‏الترتيب مقصود ويخالف الشاشة السابقة: **اليوم أولاً**. الشاشة القديمة // ‏كانت تبدأ بترويسة زخرفية بارتفاع ٢٠٠ ثم تبويبَي أسبوعي/شهري — والسائق // ‏يفتح الشاشة ليعرف كم كسب اليوم، لا ليقرأ تقريراً شهرياً. // // ‏ثم الاقتطاعات، ثم أسبوعه، ثم تفصيل كل رحلة. import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:siro_driver/constant/currency.dart'; import 'package:siro_driver/constant/finance_design_system.dart'; import 'package:siro_driver/controller/gamification/gamification_controller.dart'; import 'package:siro_driver/controller/home/statistics/earnings_controller.dart'; import 'package:siro_driver/controller/home/statistics/earnings_models.dart'; import '../../widgets/ui/siro_ui.dart'; import 'widgets/behavior_and_achievements.dart'; import 'widgets/daily_goal_widget.dart'; import 'widgets/level_progress_widget.dart'; import 'widgets/stat_summary_card.dart'; class EarningsPage extends StatelessWidget { EarningsPage({super.key}); // ‏Get.put هنا لا في build: الشاشة السابقة كانت تنادي التحديث داخل // ‏build، وbuild يعمل عشرات المرات — أي نداء شبكي مع كل إعادة رسم. final EarningsController c = Get.put(EarningsController()); final GamificationController gam = Get.put(GamificationController()); @override Widget build(BuildContext context) { // شريط العنوان يأتي من الثيم — كان يبني ألوانه يدوياً في كل صفحة، // فيختلف ارتفاعه ووزن خطه من شاشة لأخرى. return Scaffold( appBar: AppBar(title: Text('My Earnings'.tr)), body: GetBuilder( builder: (ctl) { if (ctl.isLoading && ctl.summary == null) { return const SiroLoading(); } // ‏لا بيانات ولا كاش: نعرض الخطأ صريحاً مع زر إعادة. الشاشة // ‏السابقة كانت تعرض سبعة أيام بأصفار في هذه الحالة — فيرى // ‏السائق دخله وقد تبخّر، وهي أسوأ رسالة يمكن أن يتلقّاها. if (ctl.summary == null) { return _errorState(ctl); } final s = ctl.summary!; // ‏الترتيب هو القرار الأهم في هذه الشاشة بعد دمج الإحصائيات // ‏فيها: **المال أولاً، ثم التقدّم**. السائق يفتحها ليعرف كم // ‏كسب، لا ليرى مستواه — ووضع التلعيب قبل الأرباح كان يجعله // ‏يمرّر ثلاث شاشات ليصل إلى ما جاء من أجله. return RefreshIndicator( onRefresh: () async { await ctl.load(force: true); await gam.fetchGamificationData(force: true); }, child: ListView( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.symmetric( horizontal: FinanceDesignSystem.horizontalPadding, vertical: 8), children: [ if (ctl.errorMessage != null) _staleBanner(ctl), _todayCard(s, ctl), const SizedBox(height: 16), if (s.deductions.hasAny) ...[ _deductionsCard(s.deductions), const SizedBox(height: 16), ], _weekCard(s, ctl), const SizedBox(height: 16), _monthCard(s), const SizedBox(height: 16), _tripsSection(ctl), const SizedBox(height: 24), _progressSection(), const SizedBox(height: 40), ], ), ); }, ), ); } /// ‏قسم التقدّم — ما كان شاشة «الإحصائيات» المنفصلة. /// /// ‏في GetBuilder خاصّ به: بيانات التلعيب تصل من نقطة أخرى وفي وقت /// ‏آخر، وربطها بمُنشئ الأرباح كان سيعيد رسم الشاشة كلها — بأرقام /// ‏المال وقوائم الرحلات — كلما تحدّث عدّاد إنجاز. Widget _progressSection() => GetBuilder( builder: (gc) { if (gc.isLoading) { return const Padding( padding: EdgeInsets.symmetric(vertical: 24), child: Center(child: CircularProgressIndicator()), ); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('تقدّمي', style: FinanceDesignSystem.headingStyle), const SizedBox(height: 12), // ‏هدف اليوم بعد الأرباح مباشرةً منطقياً — لكنه هنا لأنه // ‏تحفيز لا معلومة: مكانه بعد أن يعرف السائق أرقامه. DailyGoalWidget(controller: gc), const SizedBox(height: 16), LevelProgressWidget(controller: gc), const SizedBox(height: 16), GridView.count( crossAxisCount: 2, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), crossAxisSpacing: 12, mainAxisSpacing: 12, childAspectRatio: 1.5, children: [ StatSummaryCard( icon: Icons.local_taxi_rounded, label: 'Total Trips'.tr, value: gc.totalTrips.toString(), color: FinanceDesignSystem.accentBlue, ), StatSummaryCard( icon: Icons.star_rounded, label: 'Rating'.tr, value: gc.averageRating.toStringAsFixed(1), color: const Color(0xFFFFD700), ), StatSummaryCard( icon: Icons.whatshot_rounded, label: 'Day Streak'.tr, value: '${gc.consecutiveDays}', color: const Color(0xFFFF5722), ), StatSummaryCard( icon: Icons.people_rounded, label: 'Referrals'.tr, value: gc.totalReferrals.toString(), color: const Color(0xFF9C27B0), ), ], ), const SizedBox(height: 16), BehaviorSection(gc: gc), const SizedBox(height: 24), AchievementsSection(gc: gc), ], ); }, ); Widget _errorState(EarningsController ctl) => Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.cloud_off_rounded, size: 48, color: FinanceDesignSystem.textMuted), const SizedBox(height: 12), Text( ctl.errorMessage ?? 'تعذّر تحميل أرباحك', textAlign: TextAlign.center, style: TextStyle( color: FinanceDesignSystem.primaryDark, fontSize: 15), ), const SizedBox(height: 6), Text( 'أرباحك محفوظة ولم يضع منها شيء — المشكلة في التحميل فقط.', textAlign: TextAlign.center, style: TextStyle( color: FinanceDesignSystem.textSecondary, fontSize: 12), ), const SizedBox(height: 20), ElevatedButton.icon( onPressed: () => ctl.load(force: true), icon: const Icon(Icons.refresh_rounded), label: const Text('إعادة المحاولة'), ), ], ), ), ); /// ‏بيانات قديمة معروضة بعد فشل التحديث. إظهار ذلك صراحةً أصدق من /// ‏تقديم رقمٍ عمره ساعات على أنه لحظته. Widget _staleBanner(EarningsController ctl) => Container( margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: Colors.orange.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(FinanceDesignSystem.mainRadius), ), child: Row( children: [ const Icon(Icons.history_rounded, color: Colors.orange, size: 18), const SizedBox(width: 8), Expanded( child: Text( '${ctl.errorMessage} — هذه أرقام ${ctl.lastUpdatedLabel}', style: const TextStyle(color: Colors.orange, fontSize: 12), ), ), TextButton( onPressed: () => ctl.load(force: true), child: const Text('تحديث', style: TextStyle(fontSize: 12)), ), ], ), ); /// ‏بطاقة اليوم — الرقم الذي فُتحت الشاشة من أجله. Widget _todayCard(EarningsSummary s, EarningsController ctl) { final t = s.today; return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( gradient: FinanceDesignSystem.balanceGradient, borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius), boxShadow: [FinanceDesignSystem.softShadow], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Text('أرباح اليوم', style: TextStyle(color: Colors.white70, fontSize: 13)), const Spacer(), Text(ctl.lastUpdatedLabel, style: const TextStyle(color: Colors.white54, fontSize: 11)), ], ), const SizedBox(height: 6), Text('${t.net.toStringAsFixed(2)} ${CurrencyHelper.currency}', style: const TextStyle( color: Colors.white, fontSize: 34, fontWeight: FontWeight.bold)), const SizedBox(height: 14), Row( children: [ _miniStat(Icons.local_taxi_rounded, '${t.trips}', 'رحلة'), _miniStat(Icons.schedule_rounded, t.hours.toStringAsFixed(1), 'ساعة'), _miniStat(Icons.route_rounded, t.distance.toStringAsFixed(0), 'كم'), ], ), // ‏تركيبة المبلغ ظاهرة: إجمالي ← عمولة ← صافي. السائق الذي يرى // ‏التركيبة يفهم، والذي يرى الصافي وحده يخمّن ويشكّ. if (t.gross > 0) ...[ const SizedBox(height: 14), Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _breakdownBit('دفع الركاب', t.gross), _breakdownBit('عمولة المنصة', t.commission, dim: true), _breakdownBit('لك', t.net, bold: true), ], ), ), ], ], ), ); } Widget _miniStat(IconData icon, String value, String label) => Expanded( child: Row( children: [ Icon(icon, color: Colors.white70, size: 16), const SizedBox(width: 6), Text(value, style: const TextStyle( color: Colors.white, fontSize: 15, fontWeight: FontWeight.w700)), const SizedBox(width: 3), Text(label, style: const TextStyle(color: Colors.white54, fontSize: 11)), ], ), ); Widget _breakdownBit(String label, double value, {bool dim = false, bool bold = false}) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: TextStyle( color: Colors.white.withValues(alpha: 0.6), fontSize: 10)), const SizedBox(height: 2), Text(value.toStringAsFixed(2), style: TextStyle( color: dim ? Colors.white.withValues(alpha: 0.7) : Colors.white, fontSize: bold ? 15 : 13, fontWeight: bold ? FontWeight.bold : FontWeight.w500)), ], ); /// ‏الاقتطاعات بجانب الأرباح التي اقتُطعت منها. عرضها في شاشة المنتج /// ‏وحدها كان سيجعل الخصم مفاجأةً — وهي أخطر ما يصيب منتجاً ائتمانياً. Widget _deductionsCard(Deductions d) => Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: FinanceDesignSystem.cardColor, borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius), border: Border.all(color: FinanceDesignSystem.borderColor), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.account_balance_wallet_rounded, size: 18, color: FinanceDesignSystem.accentBlue), const SizedBox(width: 8), Text('الاقتطاعات', style: FinanceDesignSystem.headingStyle), const Spacer(), if (d.today > 0) Text('اليوم ${d.today.toStringAsFixed(2)}', style: TextStyle( color: FinanceDesignSystem.textSecondary, fontSize: 12)), ], ), const SizedBox(height: 10), ...d.items.map((i) => Padding( padding: const EdgeInsets.only(bottom: 6), child: Row( children: [ Expanded( child: Text(i.name, style: TextStyle( color: FinanceDesignSystem.primaryDark, fontSize: 13))), Text( 'متبقٍّ ${i.outstanding.toStringAsFixed(2)} ${CurrencyHelper.currency}', style: TextStyle( color: FinanceDesignSystem.textSecondary, fontSize: 12)), ], ), )), if (d.outstanding > 0) ...[ const SizedBox(height: 6), Text( 'يُقتطع تدريجياً من أرباحك بسقف يومي، ولا يُخصم منك في يوم بلا دخل.', style: TextStyle( color: FinanceDesignSystem.textSecondary, fontSize: 11), ), ], ], ), ); Widget _weekCard(EarningsSummary s, EarningsController ctl) { final maxNet = s.daily.fold( 0, (m, d) => d.net > m ? d.net : m); return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: FinanceDesignSystem.cardColor, borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius), border: Border.all(color: FinanceDesignSystem.borderColor), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text('آخر ٧ أيام', style: FinanceDesignSystem.headingStyle), const Spacer(), if (s.trendPercent != null) _trendChip(s.trendPercent!), ], ), const SizedBox(height: 4), Text( '${s.week.net.toStringAsFixed(2)} ${CurrencyHelper.currency} · ${s.week.trips} رحلة', style: TextStyle( color: FinanceDesignSystem.textSecondary, fontSize: 13)), const SizedBox(height: 16), SizedBox( height: 110, child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: s.daily.map((d) { final selected = ctl.selectedDay == d.day; final ratio = maxNet > 0 ? d.net / maxNet : 0.0; return Expanded( child: GestureDetector( // ‏نقر العمود يعرض رحلات ذلك اليوم. الرسم البياني الذي // ‏لا يُنقر عليه زينة؛ والسائق يسأل عن يومٍ بعينه. onTap: () => ctl.loadTrips(day: selected ? null : d.day), child: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ Text(d.net > 0 ? d.net.toStringAsFixed(0) : '', style: TextStyle( color: FinanceDesignSystem.textSecondary, fontSize: 9)), const SizedBox(height: 4), Container( height: 60 * (ratio == 0 ? 0.03 : ratio), margin: const EdgeInsets.symmetric(horizontal: 3), decoration: BoxDecoration( color: selected ? FinanceDesignSystem.accentBlue : FinanceDesignSystem.accentBlue .withValues(alpha: 0.35), borderRadius: BorderRadius.circular(6), ), ), const SizedBox(height: 6), Text(_dayLabel(d.day), style: TextStyle( color: FinanceDesignSystem.textSecondary, fontSize: 10)), ], ), ), ); }).toList(), ), ), ], ), ); } /// ‏الشهر — يحلّ محل الرسم الشهري في الشاشة القديمة. /// /// ‏أرقام لا رسم بياني: الرسم الشهري كان ثلاثين عموداً ضيّقاً لا يُقرأ /// ‏منها شيء على شاشة هاتف. السائق يسأل عن مجموع شهره ومعدّله اليومي، /// ‏لا عن شكل توزيعه. Widget _monthCard(EarningsSummary s) { final m = s.month; final avgPerDay = DateTime.now().day > 0 ? m.net / DateTime.now().day : 0.0; final perHour = m.hours > 0 ? m.net / m.hours : 0.0; return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: FinanceDesignSystem.cardColor, borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius), border: Border.all(color: FinanceDesignSystem.borderColor), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('هذا الشهر', style: FinanceDesignSystem.headingStyle), const SizedBox(height: 12), Row( children: [ _monthStat('الصافي', m.net.toStringAsFixed(0)), _monthStat('رحلة', '${m.trips}'), _monthStat('ساعة', m.hours.toStringAsFixed(0)), ], ), const SizedBox(height: 12), Row( children: [ // ‏معدّل الساعة هو الرقم الذي يقرّر به السائق متى يعمل — // ‏وأنفع من أي مجموع، لأن المجموع يكافئ الساعات لا الكفاءة. _monthStat('معدّل الساعة', perHour.toStringAsFixed(1)), _monthStat('معدّل اليوم', avgPerDay.toStringAsFixed(1)), _monthStat('العمولة', m.commission.toStringAsFixed(0)), ], ), ], ), ); } Widget _monthStat(String label, String value) => Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(value, style: TextStyle( color: FinanceDesignSystem.primaryDark, fontSize: 17, fontWeight: FontWeight.bold)), const SizedBox(height: 2), Text(label, style: TextStyle( color: FinanceDesignSystem.textSecondary, fontSize: 11)), ], ), ); Widget _trendChip(double trend) { final up = trend >= 0; final color = up ? FinanceDesignSystem.successGreen : FinanceDesignSystem.dangerRed; return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: color.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(up ? Icons.trending_up_rounded : Icons.trending_down_rounded, size: 14, color: color), const SizedBox(width: 4), // ‏المقارنة بأسبوعه هو لا بالسائقين الآخرين. Text('${trend.abs().toStringAsFixed(0)}٪ عن الأسبوع الماضي', style: TextStyle(color: color, fontSize: 11)), ], ), ); } String _dayLabel(String iso) { final d = DateTime.tryParse(iso); if (d == null) return ''; const names = ['اثن', 'ثلا', 'أرب', 'خمي', 'جمع', 'سبت', 'أحد']; return names[d.weekday - 1]; } Widget _tripsSection(EarningsController ctl) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( ctl.selectedDay == null ? 'رحلات الأسبوع' : 'رحلات ${ctl.selectedDay}', style: FinanceDesignSystem.headingStyle), const Spacer(), if (ctl.selectedDay != null) TextButton( onPressed: () => ctl.loadTrips(), child: const Text('كل الأسبوع', style: TextStyle(fontSize: 12))), ], ), const SizedBox(height: 8), if (ctl.isLoadingTrips) const Padding( padding: EdgeInsets.all(20), child: Center(child: CircularProgressIndicator()), ) else if (ctl.trips.isEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 20), child: Center( child: Text('لا رحلات في هذه الفترة', style: TextStyle( color: FinanceDesignSystem.textSecondary, fontSize: 13)), ), ) else ...ctl.trips.map(_tripTile), ], ); /// ‏تفصيل الرحلة الواحدة — الإجابة على السؤال الأول عند كل سائق: /// ‏«لماذا أخذت هذا المبلغ من هذه الرحلة؟» Widget _tripTile(TripEarning t) { final b = t.breakdown; return Container( margin: const EdgeInsets.only(bottom: 8), decoration: BoxDecoration( color: FinanceDesignSystem.cardColor, borderRadius: BorderRadius.circular(FinanceDesignSystem.mainRadius), border: Border.all(color: FinanceDesignSystem.borderColor), ), child: Theme( data: ThemeData(dividerColor: Colors.transparent), child: ExpansionTile( tilePadding: const EdgeInsets.symmetric(horizontal: 14), childrenPadding: const EdgeInsets.fromLTRB(14, 0, 14, 12), title: Row( children: [ Expanded( child: Text( '${t.from} ← ${t.to}', maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: FinanceDesignSystem.primaryDark, fontSize: 13, fontWeight: FontWeight.w600), ), ), const SizedBox(width: 8), Text('${b.net.toStringAsFixed(2)} ${CurrencyHelper.currency}', style: TextStyle( color: FinanceDesignSystem.successGreen, fontWeight: FontWeight.bold, fontSize: 14)), ], ), subtitle: Padding( padding: const EdgeInsets.only(top: 4), child: Row( children: [ Text( '${_timeLabel(t.at)} · ${t.distanceKm.toStringAsFixed(1)} كم · ${t.minutes} د', style: TextStyle( color: FinanceDesignSystem.textSecondary, fontSize: 11), ), if (b.zeroCommission) ...[ const SizedBox(width: 6), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1), decoration: BoxDecoration( color: FinanceDesignSystem.successGreen .withValues(alpha: 0.15), borderRadius: BorderRadius.circular(6), ), child: Text('عمولة معفاة', style: TextStyle( color: FinanceDesignSystem.successGreen, fontSize: 9)), ), ], ], ), ), children: [ _row('ما دفعه الراكب', b.gross), if (b.bonus > 0) _row('مكافأة', b.bonus, positive: true), _row('عمولة المنصة', b.commission, negative: true), Divider(color: FinanceDesignSystem.borderColor, height: 16), _row('صافي لك', b.net, bold: true), const SizedBox(height: 6), Row( children: [ Icon( t.paymentMethod.toLowerCase().contains('cash') ? Icons.money_rounded : Icons.credit_card_rounded, size: 13, color: FinanceDesignSystem.textMuted), const SizedBox(width: 5), Text(t.paymentMethod, style: TextStyle( color: FinanceDesignSystem.textMuted, fontSize: 11)), const Spacer(), Text('#${t.rideId}', style: TextStyle( color: FinanceDesignSystem.textMuted, fontSize: 11)), ], ), ], ), ), ); } Widget _row(String label, double value, {bool bold = false, bool negative = false, bool positive = false}) => Padding( padding: const EdgeInsets.symmetric(vertical: 3), child: Row( children: [ Expanded( child: Text(label, style: TextStyle( color: FinanceDesignSystem.textSecondary, fontSize: bold ? 13 : 12)), ), Text( '${negative ? '− ' : positive ? '+ ' : ''}${value.toStringAsFixed(2)}', style: TextStyle( color: negative ? FinanceDesignSystem.dangerRed : positive ? FinanceDesignSystem.successGreen : FinanceDesignSystem.primaryDark, fontSize: bold ? 14 : 12, fontWeight: bold ? FontWeight.bold : FontWeight.w500, ), ), ], ), ); String _timeLabel(String at) { final d = DateTime.tryParse(at); if (d == null) return ''; final h = d.hour.toString().padLeft(2, '0'); final m = d.minute.toString().padLeft(2, '0'); return '$h:$m'; } }